prefect-client 2.18.0__py3-none-any.whl → 2.18.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.
Files changed (45) hide show
  1. prefect/_internal/schemas/fields.py +31 -12
  2. prefect/blocks/core.py +1 -1
  3. prefect/blocks/notifications.py +2 -2
  4. prefect/blocks/system.py +2 -3
  5. prefect/client/orchestration.py +283 -22
  6. prefect/client/schemas/sorting.py +9 -0
  7. prefect/client/utilities.py +25 -3
  8. prefect/concurrency/asyncio.py +11 -5
  9. prefect/concurrency/events.py +3 -3
  10. prefect/concurrency/services.py +1 -1
  11. prefect/concurrency/sync.py +9 -5
  12. prefect/deployments/deployments.py +27 -18
  13. prefect/deployments/runner.py +34 -26
  14. prefect/engine.py +3 -1
  15. prefect/events/actions.py +2 -1
  16. prefect/events/cli/automations.py +47 -9
  17. prefect/events/clients.py +50 -18
  18. prefect/events/filters.py +30 -3
  19. prefect/events/instrument.py +40 -40
  20. prefect/events/related.py +2 -1
  21. prefect/events/schemas/automations.py +50 -5
  22. prefect/events/schemas/deployment_triggers.py +15 -227
  23. prefect/events/schemas/events.py +7 -7
  24. prefect/events/utilities.py +1 -1
  25. prefect/events/worker.py +10 -7
  26. prefect/flows.py +33 -18
  27. prefect/input/actions.py +9 -9
  28. prefect/input/run_input.py +49 -37
  29. prefect/new_flow_engine.py +293 -0
  30. prefect/new_task_engine.py +374 -0
  31. prefect/results.py +3 -2
  32. prefect/runner/runner.py +3 -2
  33. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +44 -3
  34. prefect/settings.py +26 -0
  35. prefect/states.py +25 -19
  36. prefect/tasks.py +17 -0
  37. prefect/utilities/asyncutils.py +37 -0
  38. prefect/utilities/engine.py +6 -4
  39. prefect/utilities/schema_tools/validation.py +1 -1
  40. {prefect_client-2.18.0.dist-info → prefect_client-2.18.1.dist-info}/METADATA +1 -1
  41. {prefect_client-2.18.0.dist-info → prefect_client-2.18.1.dist-info}/RECORD +44 -43
  42. prefect/concurrency/common.py +0 -0
  43. {prefect_client-2.18.0.dist-info → prefect_client-2.18.1.dist-info}/LICENSE +0 -0
  44. {prefect_client-2.18.0.dist-info → prefect_client-2.18.1.dist-info}/WHEEL +0 -0
  45. {prefect_client-2.18.0.dist-info → prefect_client-2.18.1.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ from typing import Optional
3
3
  from uuid import UUID
4
4
 
5
5
  import pendulum
6
+ from typing_extensions import TypeAlias
6
7
 
7
8
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
8
9
 
@@ -12,19 +13,37 @@ else:
12
13
  from pydantic import BaseModel, Field
13
14
 
14
15
 
15
- class DateTimeTZ(pendulum.DateTime):
16
- @classmethod
17
- def __get_validators__(cls):
18
- yield cls.validate
16
+ # Rather than subclassing pendulum.DateTime to add our pydantic-specific validation,
17
+ # which will lead to a lot of funky typing issues, we'll just monkeypatch the pydantic
18
+ # validators onto the class. Retaining this type alias means that we can still use it
19
+ # as we have been in class definitions, also guaranteeing that we'll be applying these
20
+ # validators by importing this module.
19
21
 
20
- @classmethod
21
- def validate(cls, v):
22
- if isinstance(v, str):
23
- return pendulum.parse(v)
24
- elif isinstance(v, datetime.datetime):
25
- return pendulum.instance(v)
26
- else:
27
- raise ValueError("Unrecognized datetime.")
22
+ DateTimeTZ: TypeAlias = pendulum.DateTime
23
+
24
+
25
+ def _datetime_patched_classmethod(function):
26
+ if hasattr(DateTimeTZ, function.__name__):
27
+ return function
28
+ setattr(DateTimeTZ, function.__name__, classmethod(function))
29
+ return function
30
+
31
+
32
+ @_datetime_patched_classmethod
33
+ def __get_validators__(cls):
34
+ yield getattr(cls, "validate")
35
+
36
+
37
+ @_datetime_patched_classmethod
38
+ def validate(cls, v) -> pendulum.DateTime:
39
+ if isinstance(v, str):
40
+ parsed = pendulum.parse(v)
41
+ assert isinstance(parsed, pendulum.DateTime)
42
+ return parsed
43
+ elif isinstance(v, datetime.datetime):
44
+ return pendulum.instance(v)
45
+ else:
46
+ raise ValueError("Unrecognized datetime.")
28
47
 
29
48
 
30
49
  class CreatedBy(BaseModel):
prefect/blocks/core.py CHANGED
@@ -996,7 +996,7 @@ class Block(BaseModel, ABC):
996
996
  return self._block_document_id
997
997
 
998
998
  @sync_compatible
999
- @instrument_instance_method_call()
999
+ @instrument_instance_method_call
1000
1000
  async def save(
1001
1001
  self,
1002
1002
  name: Optional[str] = None,
@@ -62,7 +62,7 @@ class AbstractAppriseNotificationBlock(NotificationBlock, ABC):
62
62
  self._start_apprise_client(self.url)
63
63
 
64
64
  @sync_compatible
65
- @instrument_instance_method_call()
65
+ @instrument_instance_method_call
66
66
  async def notify(
67
67
  self,
68
68
  body: str,
@@ -717,7 +717,7 @@ class CustomWebhookNotificationBlock(NotificationBlock):
717
717
  raise KeyError(f"{name}/{placeholder}")
718
718
 
719
719
  @sync_compatible
720
- @instrument_instance_method_call()
720
+ @instrument_instance_method_call
721
721
  async def notify(self, body: str, subject: Optional[str] = None):
722
722
  import httpx
723
723
 
prefect/blocks/system.py CHANGED
@@ -1,7 +1,5 @@
1
1
  from typing import Any
2
2
 
3
- import pendulum
4
-
5
3
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
6
4
 
7
5
  if HAS_PYDANTIC_V2:
@@ -9,6 +7,7 @@ if HAS_PYDANTIC_V2:
9
7
  else:
10
8
  from pydantic import Field, SecretStr
11
9
 
10
+ from prefect._internal.schemas.fields import DateTimeTZ
12
11
  from prefect.blocks.core import Block
13
12
 
14
13
 
@@ -76,7 +75,7 @@ class DateTime(Block):
76
75
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/8b3da9a6621e92108b8e6a75b82e15374e170ff7-48x48.png"
77
76
  _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/system/#prefect.blocks.system.DateTime"
78
77
 
79
- value: pendulum.DateTime = Field(
78
+ value: DateTimeTZ = Field(
80
79
  default=...,
81
80
  description="An ISO 8601-compatible datetime value.",
82
81
  )
@@ -12,6 +12,7 @@ from typing import (
12
12
  Optional,
13
13
  Set,
14
14
  Tuple,
15
+ TypeVar,
15
16
  Union,
16
17
  )
17
18
  from uuid import UUID, uuid4
@@ -20,15 +21,19 @@ import certifi
20
21
  import httpcore
21
22
  import httpx
22
23
  import pendulum
24
+ from typing_extensions import ParamSpec
23
25
 
24
26
  from prefect._internal.compatibility.deprecated import (
25
27
  handle_deprecated_infra_overrides_parameter,
26
28
  )
27
29
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
30
+ from prefect.client.schemas import sorting
31
+ from prefect.events import filters
28
32
  from prefect.settings import (
29
33
  PREFECT_API_SERVICES_TRIGGERS_ENABLED,
30
34
  PREFECT_EXPERIMENTAL_EVENTS,
31
35
  )
36
+ from prefect.utilities.asyncutils import run_sync
32
37
 
33
38
  if HAS_PYDANTIC_V2:
34
39
  import pydantic.v1 as pydantic
@@ -151,6 +156,9 @@ if TYPE_CHECKING:
151
156
 
152
157
  from prefect.client.base import ASGIApp, PrefectHttpxClient, app_lifespan_context
153
158
 
159
+ P = ParamSpec("P")
160
+ R = TypeVar("R")
161
+
154
162
 
155
163
  class ServerType(AutoEnum):
156
164
  EPHEMERAL = AutoEnum.auto()
@@ -164,7 +172,9 @@ class ServerType(AutoEnum):
164
172
  return PREFECT_EXPERIMENTAL_EVENTS and PREFECT_API_SERVICES_TRIGGERS_ENABLED
165
173
 
166
174
 
167
- def get_client(httpx_settings: Optional[dict] = None) -> "PrefectClient":
175
+ def get_client(
176
+ httpx_settings: Optional[Dict[str, Any]] = None, sync_client: bool = False
177
+ ) -> "PrefectClient":
168
178
  """
169
179
  Retrieve a HTTP client for communicating with the Prefect REST API.
170
180
 
@@ -174,6 +184,13 @@ def get_client(httpx_settings: Optional[dict] = None) -> "PrefectClient":
174
184
  async with get_client() as client:
175
185
  await client.hello()
176
186
  ```
187
+
188
+ To return a synchronous client, pass sync_client=True:
189
+
190
+ ```python
191
+ with get_client(sync_client=True) as client:
192
+ client.hello()
193
+ ```
177
194
  """
178
195
  ctx = prefect.context.get_settings_context()
179
196
  api = PREFECT_API_URL.value()
@@ -184,11 +201,18 @@ def get_client(httpx_settings: Optional[dict] = None) -> "PrefectClient":
184
201
 
185
202
  api = create_app(ctx.settings, ephemeral=True)
186
203
 
187
- return PrefectClient(
188
- api,
189
- api_key=PREFECT_API_KEY.value(),
190
- httpx_settings=httpx_settings,
191
- )
204
+ if sync_client:
205
+ return SyncPrefectClient(
206
+ api,
207
+ api_key=PREFECT_API_KEY.value(),
208
+ httpx_settings=httpx_settings,
209
+ )
210
+ else:
211
+ return PrefectClient(
212
+ api,
213
+ api_key=PREFECT_API_KEY.value(),
214
+ httpx_settings=httpx_settings,
215
+ )
192
216
 
193
217
 
194
218
  class PrefectClient:
@@ -2136,21 +2160,23 @@ class PrefectClient:
2136
2160
 
2137
2161
  async def create_task_run(
2138
2162
  self,
2139
- task: "TaskObject",
2163
+ task: "TaskObject[P, R]",
2140
2164
  flow_run_id: Optional[UUID],
2141
2165
  dynamic_key: str,
2142
- name: str = None,
2143
- extra_tags: Iterable[str] = None,
2144
- state: prefect.states.State = None,
2145
- task_inputs: Dict[
2146
- str,
2147
- List[
2148
- Union[
2149
- TaskRunResult,
2150
- Parameter,
2151
- Constant,
2152
- ]
2153
- ],
2166
+ name: Optional[str] = None,
2167
+ extra_tags: Optional[Iterable[str]] = None,
2168
+ state: Optional[prefect.states.State[R]] = None,
2169
+ task_inputs: Optional[
2170
+ Dict[
2171
+ str,
2172
+ List[
2173
+ Union[
2174
+ TaskRunResult,
2175
+ Parameter,
2176
+ Constant,
2177
+ ]
2178
+ ],
2179
+ ]
2154
2180
  ] = None,
2155
2181
  ) -> TaskRun:
2156
2182
  """
@@ -3143,9 +3169,7 @@ class PrefectClient:
3143
3169
  response.raise_for_status()
3144
3170
  return pydantic.parse_obj_as(List[Automation], response.json())
3145
3171
 
3146
- async def find_automation(
3147
- self, id_or_name: str, exit_if_not_found: bool = True
3148
- ) -> Optional[Automation]:
3172
+ async def find_automation(self, id_or_name: str) -> Optional[Automation]:
3149
3173
  try:
3150
3174
  id = UUID(id_or_name)
3151
3175
  except ValueError:
@@ -3180,6 +3204,34 @@ class PrefectClient:
3180
3204
  response.raise_for_status()
3181
3205
  return Automation.parse_obj(response.json())
3182
3206
 
3207
+ async def read_automations_by_name(self, name: str) -> List[Automation]:
3208
+ """
3209
+ Query the Prefect API for an automation by name. Only automations matching the provided name will be returned.
3210
+
3211
+ Args:
3212
+ name: the name of the automation to query
3213
+
3214
+ Returns:
3215
+ a list of Automation model representations of the automations
3216
+ """
3217
+ if not self.server_type.supports_automations():
3218
+ self._raise_for_unsupported_automations()
3219
+ automation_filter = filters.AutomationFilter(name=dict(any_=[name]))
3220
+
3221
+ response = await self._client.post(
3222
+ "/automations/filter",
3223
+ json={
3224
+ "sort": sorting.AutomationSort.UPDATED_DESC,
3225
+ "automations": automation_filter.dict(json_compatible=True)
3226
+ if automation_filter
3227
+ else None,
3228
+ },
3229
+ )
3230
+
3231
+ response.raise_for_status()
3232
+
3233
+ return pydantic.parse_obj_as(List[Automation], response.json())
3234
+
3183
3235
  async def pause_automation(self, automation_id: UUID):
3184
3236
  if not self.server_type.supports_automations():
3185
3237
  self._raise_for_unsupported_automations()
@@ -3284,3 +3336,212 @@ class PrefectClient:
3284
3336
 
3285
3337
  def __exit__(self, *_):
3286
3338
  assert False, "This should never be called but must be defined for __enter__"
3339
+
3340
+
3341
+ class SyncPrefectClient:
3342
+ """
3343
+ A synchronous client for interacting with the [Prefect REST API](/api-ref/rest-api/).
3344
+
3345
+ Args:
3346
+ api: the REST API URL or FastAPI application to connect to
3347
+ api_key: An optional API key for authentication.
3348
+ api_version: The API version this client is compatible with.
3349
+ httpx_settings: An optional dictionary of settings to pass to the underlying
3350
+ `httpx.AsyncClient`
3351
+
3352
+ Examples:
3353
+
3354
+ Say hello to a Prefect REST API
3355
+
3356
+ <div class="terminal">
3357
+ ```
3358
+ >>> with get_client(sync_client=True) as client:
3359
+ >>> response = client.hello()
3360
+ >>>
3361
+ >>> print(response.json())
3362
+ 👋
3363
+ ```
3364
+ </div>
3365
+ """
3366
+
3367
+ def __init__(
3368
+ self,
3369
+ api: Union[str, ASGIApp],
3370
+ *,
3371
+ api_key: Optional[str] = None,
3372
+ api_version: Optional[str] = None,
3373
+ httpx_settings: Optional[Dict[str, Any]] = None,
3374
+ ) -> None:
3375
+ self._prefect_client = PrefectClient(
3376
+ api=api,
3377
+ api_key=api_key,
3378
+ api_version=api_version,
3379
+ httpx_settings=httpx_settings,
3380
+ )
3381
+
3382
+ def __enter__(self):
3383
+ run_sync(self._prefect_client.__aenter__())
3384
+ return self
3385
+
3386
+ def __exit__(self, *exc_info):
3387
+ return run_sync(self._prefect_client.__aexit__(*exc_info))
3388
+
3389
+ async def __aenter__(self):
3390
+ raise RuntimeError(
3391
+ "The `SyncPrefectClient` must be entered with a sync context. Use '"
3392
+ "with SyncPrefectClient(...)' not 'async with SyncPrefectClient(...)'"
3393
+ )
3394
+
3395
+ async def __aexit__(self, *_):
3396
+ assert False, "This should never be called but must be defined for __aenter__"
3397
+
3398
+ def hello(self) -> httpx.Response:
3399
+ """
3400
+ Send a GET request to /hello for testing purposes.
3401
+ """
3402
+ return run_sync(self._prefect_client.hello())
3403
+
3404
+ def create_task_run(
3405
+ self,
3406
+ task: "TaskObject[P, R]",
3407
+ flow_run_id: Optional[UUID],
3408
+ dynamic_key: str,
3409
+ name: Optional[str] = None,
3410
+ extra_tags: Optional[Iterable[str]] = None,
3411
+ state: Optional[prefect.states.State[R]] = None,
3412
+ task_inputs: Optional[
3413
+ Dict[
3414
+ str,
3415
+ List[
3416
+ Union[
3417
+ TaskRunResult,
3418
+ Parameter,
3419
+ Constant,
3420
+ ]
3421
+ ],
3422
+ ]
3423
+ ] = None,
3424
+ ) -> TaskRun:
3425
+ """
3426
+ Create a task run
3427
+
3428
+ Args:
3429
+ task: The Task to run
3430
+ flow_run_id: The flow run id with which to associate the task run
3431
+ dynamic_key: A key unique to this particular run of a Task within the flow
3432
+ name: An optional name for the task run
3433
+ extra_tags: an optional list of extra tags to apply to the task run in
3434
+ addition to `task.tags`
3435
+ state: The initial state for the run. If not provided, defaults to
3436
+ `Pending` for now. Should always be a `Scheduled` type.
3437
+ task_inputs: the set of inputs passed to the task
3438
+
3439
+ Returns:
3440
+ The created task run.
3441
+ """
3442
+ return run_sync(
3443
+ self._prefect_client.create_task_run(
3444
+ task=task,
3445
+ flow_run_id=flow_run_id,
3446
+ dynamic_key=dynamic_key,
3447
+ name=name,
3448
+ extra_tags=extra_tags,
3449
+ state=state,
3450
+ task_inputs=task_inputs,
3451
+ )
3452
+ )
3453
+
3454
+ def set_task_run_state(
3455
+ self,
3456
+ task_run_id: UUID,
3457
+ state: prefect.states.State,
3458
+ force: bool = False,
3459
+ ) -> OrchestrationResult:
3460
+ """
3461
+ Set the state of a task run.
3462
+
3463
+ Args:
3464
+ task_run_id: the id of the task run
3465
+ state: the state to set
3466
+ force: if True, disregard orchestration logic when setting the state,
3467
+ forcing the Prefect API to accept the state
3468
+
3469
+ Returns:
3470
+ an OrchestrationResult model representation of state orchestration output
3471
+ """
3472
+ return run_sync(
3473
+ self._prefect_client.set_task_run_state(
3474
+ task_run_id=task_run_id,
3475
+ state=state,
3476
+ force=force,
3477
+ )
3478
+ )
3479
+
3480
+ def create_flow_run(
3481
+ self,
3482
+ flow_id: UUID,
3483
+ parameters: Optional[Dict[str, Any]] = None,
3484
+ context: Optional[Dict[str, Any]] = None,
3485
+ scheduled_start_time: Optional[datetime.datetime] = None,
3486
+ run_name: Optional[str] = None,
3487
+ labels: Optional[List[str]] = None,
3488
+ parameters_json: Optional[str] = None,
3489
+ run_config: Optional[Dict[str, Any]] = None,
3490
+ idempotency_key: Optional[str] = None,
3491
+ ) -> FlowRunResponse:
3492
+ """
3493
+ Create a new flow run.
3494
+
3495
+ Args:
3496
+ - flow_id (UUID): the ID of the flow to create a run for
3497
+ - parameters (Optional[Dict[str, Any]]): a dictionary of parameter values to pass to the flow
3498
+ - context (Optional[Dict[str, Any]]): a dictionary of context values to pass to the flow
3499
+ - scheduled_start_time (Optional[datetime.datetime]): the scheduled start time for the flow run
3500
+ - run_name (Optional[str]): a name to assign to the flow run
3501
+ - labels (Optional[List[str]]): a list of labels to assign to the flow run
3502
+ - parameters_json (Optional[str]): a JSON string of parameter values to pass to the flow
3503
+ - run_config (Optional[Dict[str, Any]]): a dictionary of run configuration options
3504
+ - idempotency_key (Optional[str]): a key to ensure idempotency when creating the flow run
3505
+
3506
+ Returns:
3507
+ - FlowRunResponse: the created flow run
3508
+ """
3509
+ return run_sync(
3510
+ self._prefect_client.create_flow_run(
3511
+ flow_id=flow_id,
3512
+ parameters=parameters,
3513
+ context=context,
3514
+ scheduled_start_time=scheduled_start_time,
3515
+ run_name=run_name,
3516
+ labels=labels,
3517
+ parameters_json=parameters_json,
3518
+ run_config=run_config,
3519
+ idempotency_key=idempotency_key,
3520
+ )
3521
+ )
3522
+
3523
+ async def set_flow_run_state(
3524
+ self,
3525
+ flow_run_id: UUID,
3526
+ state: "prefect.states.State",
3527
+ force: bool = False,
3528
+ ) -> OrchestrationResult:
3529
+ """
3530
+ Set the state of a flow run.
3531
+
3532
+ Args:
3533
+ flow_run_id: the id of the flow run
3534
+ state: the state to set
3535
+ force: if True, disregard orchestration logic when setting the state,
3536
+ forcing the Prefect API to accept the state
3537
+
3538
+ Returns:
3539
+ an OrchestrationResult model representation of state orchestration output
3540
+ """
3541
+ return run_sync(
3542
+ self._prefect_client.set_flow_run_state(
3543
+ flow_run_id=flow_run_id,
3544
+ state=state,
3545
+ force=force,
3546
+ )
3547
+ )
@@ -27,6 +27,15 @@ class TaskRunSort(AutoEnum):
27
27
  END_TIME_DESC = AutoEnum.auto()
28
28
 
29
29
 
30
+ class AutomationSort(AutoEnum):
31
+ """Defines automation sorting options."""
32
+
33
+ CREATED_DESC = AutoEnum.auto()
34
+ UPDATED_DESC = AutoEnum.auto()
35
+ NAME_ASC = AutoEnum.auto()
36
+ NAME_DESC = AutoEnum.auto()
37
+
38
+
30
39
  class LogSort(AutoEnum):
31
40
  """Defines log sorting options."""
32
41
 
@@ -6,14 +6,25 @@ Utilities for working with clients.
6
6
  # circular imports for decorators such as `inject_client` which are widely used.
7
7
 
8
8
  from functools import wraps
9
- from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional, Tuple, cast
10
-
11
- from typing_extensions import ParamSpec
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Awaitable,
13
+ Callable,
14
+ Coroutine,
15
+ Optional,
16
+ Tuple,
17
+ TypeVar,
18
+ cast,
19
+ )
20
+
21
+ from typing_extensions import Concatenate, ParamSpec
12
22
 
13
23
  if TYPE_CHECKING:
14
24
  from prefect.client.orchestration import PrefectClient
15
25
 
16
26
  P = ParamSpec("P")
27
+ R = TypeVar("R")
17
28
 
18
29
 
19
30
  def get_or_create_client(
@@ -52,6 +63,17 @@ def get_or_create_client(
52
63
  return get_httpx_client(), False
53
64
 
54
65
 
66
+ def client_injector(
67
+ func: Callable[Concatenate["PrefectClient", P], Awaitable[R]],
68
+ ) -> Callable[P, Awaitable[R]]:
69
+ @wraps(func)
70
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
71
+ client, _ = get_or_create_client()
72
+ return await func(client, *args, **kwargs)
73
+
74
+ return wrapper
75
+
76
+
55
77
  def inject_client(
56
78
  fn: Callable[P, Coroutine[Any, Any, Any]],
57
79
  ) -> Callable[P, Coroutine[Any, Any, Any]]:
@@ -1,10 +1,16 @@
1
1
  import asyncio
2
2
  from contextlib import asynccontextmanager
3
- from typing import List, Literal, Union
3
+ from typing import List, Literal, Union, cast
4
4
 
5
5
  import httpx
6
6
  import pendulum
7
7
 
8
+ try:
9
+ from pendulum import Interval
10
+ except ImportError:
11
+ # pendulum < 3
12
+ from pendulum.period import Period as Interval # type: ignore
13
+
8
14
  from prefect import get_client
9
15
  from prefect.client.schemas.responses import MinimalConcurrencyLimitResponse
10
16
 
@@ -30,10 +36,10 @@ async def concurrency(names: Union[str, List[str]], occupy: int = 1):
30
36
  try:
31
37
  yield
32
38
  finally:
33
- occupancy_seconds: float = (
34
- pendulum.now("UTC") - acquisition_time
35
- ).total_seconds()
36
- await _release_concurrency_slots(names, occupy, occupancy_seconds)
39
+ occupancy_period = cast(Interval, (pendulum.now("UTC") - acquisition_time))
40
+ await _release_concurrency_slots(
41
+ names, occupy, occupancy_period.total_seconds()
42
+ )
37
43
  _emit_concurrency_release_events(limits, occupy, emitted_events)
38
44
 
39
45
 
@@ -12,11 +12,11 @@ def _emit_concurrency_event(
12
12
  slots: int,
13
13
  follows: Union[Event, None] = None,
14
14
  ) -> Union[Event, None]:
15
- resource = {
15
+ resource: Dict[str, str] = {
16
16
  "prefect.resource.id": f"prefect.concurrency-limit.{primary_limit.id}",
17
17
  "prefect.resource.name": primary_limit.name,
18
- "slots-acquired": slots,
19
- "limit": primary_limit.limit,
18
+ "slots-acquired": str(slots),
19
+ "limit": str(primary_limit.limit),
20
20
  }
21
21
 
22
22
  related = [
@@ -64,7 +64,7 @@ class ConcurrencySlotAcquisitionService(QueueService):
64
64
  raise RuntimeError("Cannot put items in a stopped service instance.")
65
65
 
66
66
  logger.debug("Service %r enqueuing item %r", self, item)
67
- future = concurrent.futures.Future()
67
+ future: concurrent.futures.Future = concurrent.futures.Future()
68
68
 
69
69
  occupy, mode = item
70
70
  self._queue.put_nowait((occupy, mode, future))
@@ -1,8 +1,14 @@
1
1
  from contextlib import contextmanager
2
- from typing import List, Union
2
+ from typing import List, Union, cast
3
3
 
4
4
  import pendulum
5
5
 
6
+ try:
7
+ from pendulum import Interval
8
+ except ImportError:
9
+ # pendulum < 3
10
+ from pendulum.period import Period as Interval # type: ignore
11
+
6
12
  from prefect._internal.concurrency.api import create_call, from_sync
7
13
  from prefect._internal.concurrency.event_loop import get_running_loop
8
14
  from prefect.client.schemas.responses import MinimalConcurrencyLimitResponse
@@ -30,11 +36,9 @@ def concurrency(names: Union[str, List[str]], occupy: int = 1):
30
36
  try:
31
37
  yield
32
38
  finally:
33
- occupancy_seconds: float = (
34
- pendulum.now("UTC") - acquisition_time
35
- ).total_seconds()
39
+ occupancy_period = cast(Interval, pendulum.now("UTC") - acquisition_time)
36
40
  _call_async_function_from_sync(
37
- _release_concurrency_slots, names, occupy, occupancy_seconds
41
+ _release_concurrency_slots, names, occupy, occupancy_period.total_seconds()
38
42
  )
39
43
  _emit_concurrency_release_events(limits, occupy, emitted_events)
40
44