prefect-client 2.18.1__py3-none-any.whl → 2.18.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prefect/automations.py ADDED
@@ -0,0 +1,162 @@
1
+ from typing import Optional
2
+ from uuid import UUID
3
+
4
+ from pydantic import Field
5
+ from typing_extensions import Self
6
+
7
+ from prefect.client.utilities import get_or_create_client
8
+ from prefect.events.schemas.automations import (
9
+ AutomationCore,
10
+ CompositeTrigger,
11
+ CompoundTrigger,
12
+ EventTrigger,
13
+ MetricTrigger,
14
+ MetricTriggerOperator,
15
+ MetricTriggerQuery,
16
+ Posture,
17
+ PrefectMetric,
18
+ ResourceSpecification,
19
+ ResourceTrigger,
20
+ SequenceTrigger,
21
+ Trigger,
22
+ )
23
+ from prefect.exceptions import PrefectHTTPStatusError
24
+ from prefect.utilities.asyncutils import sync_compatible
25
+
26
+ __all__ = [
27
+ "AutomationCore",
28
+ "EventTrigger",
29
+ "ResourceTrigger",
30
+ "Posture",
31
+ "Trigger",
32
+ "ResourceSpecification",
33
+ "MetricTriggerOperator",
34
+ "MetricTrigger",
35
+ "PrefectMetric",
36
+ "CompositeTrigger",
37
+ "SequenceTrigger",
38
+ "CompoundTrigger",
39
+ "MetricTriggerQuery",
40
+ ]
41
+
42
+
43
+ class Automation(AutomationCore):
44
+ id: Optional[UUID] = Field(default=None, description="The ID of this automation")
45
+
46
+ @sync_compatible
47
+ async def create(self: Self) -> Self:
48
+ """
49
+ Create a new automation.
50
+
51
+ auto_to_create = Automation(
52
+ name="woodchonk",
53
+ trigger=EventTrigger(
54
+ expect={"animal.walked"},
55
+ match={
56
+ "genus": "Marmota",
57
+ "species": "monax",
58
+ },
59
+ posture="Reactive",
60
+ threshold=3,
61
+ within=timedelta(seconds=10),
62
+ ),
63
+ actions=[CancelFlowRun()]
64
+ )
65
+ created_automation = auto_to_create.create()
66
+ """
67
+ client, _ = get_or_create_client()
68
+ automation = AutomationCore(**self.dict(exclude={"id"}))
69
+ self.id = await client.create_automation(automation=automation)
70
+ return self
71
+
72
+ @sync_compatible
73
+ async def update(self: Self):
74
+ """
75
+ Updates an existing automation.
76
+ auto = Automation.read(id=123)
77
+ auto.name = "new name"
78
+ auto.update()
79
+ """
80
+
81
+ client, _ = get_or_create_client()
82
+ automation = AutomationCore(**self.dict(exclude={"id", "owner_resource"}))
83
+ await client.update_automation(automation_id=self.id, automation=automation)
84
+
85
+ @classmethod
86
+ @sync_compatible
87
+ async def read(
88
+ cls: Self, id: Optional[UUID] = None, name: Optional[str] = None
89
+ ) -> Self:
90
+ """
91
+ Read an automation by ID or name.
92
+ automation = Automation.read(name="woodchonk")
93
+
94
+ or
95
+
96
+ automation = Automation.read(id=UUID("b3514963-02b1-47a5-93d1-6eeb131041cb"))
97
+ """
98
+ if id and name:
99
+ raise ValueError("Only one of id or name can be provided")
100
+ if not id and not name:
101
+ raise ValueError("One of id or name must be provided")
102
+ client, _ = get_or_create_client()
103
+ if id:
104
+ try:
105
+ automation = await client.read_automation(automation_id=id)
106
+ except PrefectHTTPStatusError as exc:
107
+ if exc.response.status_code == 404:
108
+ raise ValueError(f"Automation with ID {id!r} not found")
109
+ return Automation(**automation.dict())
110
+ else:
111
+ automation = await client.read_automations_by_name(name=name)
112
+ if len(automation) > 0:
113
+ return Automation(**automation[0].dict()) if automation else None
114
+ else:
115
+ raise ValueError(f"Automation with name {name!r} not found")
116
+
117
+ @sync_compatible
118
+ async def delete(self: Self) -> bool:
119
+ """
120
+ auto = Automation.read(id = 123)
121
+ auto.delete()
122
+ """
123
+ try:
124
+ client, _ = get_or_create_client()
125
+ await client.delete_automation(self.id)
126
+ return True
127
+ except PrefectHTTPStatusError as exc:
128
+ if exc.response.status_code == 404:
129
+ return False
130
+ raise
131
+
132
+ @sync_compatible
133
+ async def disable(self: Self) -> bool:
134
+ """
135
+ Disable an automation.
136
+ auto = Automation.read(id = 123)
137
+ auto.disable()
138
+ """
139
+ try:
140
+ client, _ = get_or_create_client()
141
+ await client.pause_automation(self.id)
142
+ return True
143
+ except PrefectHTTPStatusError as exc:
144
+ if exc.response.status_code == 404:
145
+ return False
146
+ raise
147
+
148
+ @sync_compatible
149
+ async def enable(self: Self) -> bool:
150
+ """
151
+ Enable an automation.
152
+ auto = Automation.read(id = 123)
153
+ auto.enable()
154
+ """
155
+ try:
156
+ client, _ = get_or_create_client()
157
+ await client.resume_automation("asd")
158
+ return True
159
+ except PrefectHTTPStatusError as exc:
160
+ if exc.response.status_code == 404:
161
+ return False
162
+ raise
@@ -623,12 +623,12 @@ class PrefectClient:
623
623
  async def create_flow_run(
624
624
  self,
625
625
  flow: "FlowObject",
626
- name: str = None,
626
+ name: Optional[str] = None,
627
627
  parameters: Optional[Dict[str, Any]] = None,
628
628
  context: Optional[Dict[str, Any]] = None,
629
- tags: Iterable[str] = None,
630
- parent_task_run_id: UUID = None,
631
- state: "prefect.states.State" = None,
629
+ tags: Optional[Iterable[str]] = None,
630
+ parent_task_run_id: Optional[UUID] = None,
631
+ state: Optional["prefect.states.State"] = None,
632
632
  ) -> FlowRun:
633
633
  """
634
634
  Create a flow run for a flow.
@@ -3161,6 +3161,16 @@ class PrefectClient:
3161
3161
 
3162
3162
  return UUID(response.json()["id"])
3163
3163
 
3164
+ async def update_automation(self, automation_id: UUID, automation: AutomationCore):
3165
+ """Updates an automation in Prefect Cloud."""
3166
+ if not self.server_type.supports_automations():
3167
+ self._raise_for_unsupported_automations()
3168
+ response = await self._client.put(
3169
+ f"/automations/{automation_id}",
3170
+ json=automation.dict(json_compatible=True, exclude_unset=True),
3171
+ )
3172
+ response.raise_for_status
3173
+
3164
3174
  async def read_automations(self) -> List[Automation]:
3165
3175
  if not self.server_type.supports_automations():
3166
3176
  self._raise_for_unsupported_automations()
@@ -3169,16 +3179,24 @@ class PrefectClient:
3169
3179
  response.raise_for_status()
3170
3180
  return pydantic.parse_obj_as(List[Automation], response.json())
3171
3181
 
3172
- async def find_automation(self, id_or_name: str) -> Optional[Automation]:
3173
- try:
3174
- id = UUID(id_or_name)
3175
- except ValueError:
3176
- id = None
3182
+ async def find_automation(
3183
+ self, id_or_name: Union[str, UUID], exit_if_not_found: bool = True
3184
+ ) -> Optional[Automation]:
3185
+ if isinstance(id_or_name, str):
3186
+ try:
3187
+ id = UUID(id_or_name)
3188
+ except ValueError:
3189
+ id = None
3190
+ elif isinstance(id_or_name, UUID):
3191
+ id = id_or_name
3177
3192
 
3178
3193
  if id:
3179
- automation = await self.read_automation(id)
3180
- if automation:
3194
+ try:
3195
+ automation = await self.read_automation(id)
3181
3196
  return automation
3197
+ except prefect.exceptions.HTTPStatusError as e:
3198
+ if e.response.status_code == status.HTTP_404_NOT_FOUND:
3199
+ raise prefect.exceptions.ObjectNotFound(http_exc=e) from e
3182
3200
 
3183
3201
  automations = await self.read_automations()
3184
3202
 
@@ -53,6 +53,7 @@ if TYPE_CHECKING:
53
53
  from prefect.deprecated.data_documents import DataDocument
54
54
  from prefect.results import BaseResult
55
55
 
56
+
56
57
  R = TypeVar("R")
57
58
 
58
59
 
@@ -120,20 +121,20 @@ class WorkQueueStatus(AutoEnum):
120
121
 
121
122
 
122
123
  class StateDetails(PrefectBaseModel):
123
- flow_run_id: UUID = None
124
- task_run_id: UUID = None
124
+ flow_run_id: Optional[UUID] = None
125
+ task_run_id: Optional[UUID] = None
125
126
  # for task runs that represent subflows, the subflow's run ID
126
- child_flow_run_id: UUID = None
127
+ child_flow_run_id: Optional[UUID] = None
127
128
  scheduled_time: DateTimeTZ = None
128
- cache_key: str = None
129
+ cache_key: Optional[str] = None
129
130
  cache_expiration: DateTimeTZ = None
130
131
  untrackable_result: bool = False
131
132
  pause_timeout: DateTimeTZ = None
132
133
  pause_reschedule: bool = False
133
- pause_key: str = None
134
+ pause_key: Optional[str] = None
134
135
  run_input_keyset: Optional[Dict[str, str]] = None
135
- refresh_cache: bool = None
136
- retriable: bool = None
136
+ refresh_cache: Optional[bool] = None
137
+ retriable: Optional[bool] = None
137
138
  transition_id: Optional[UUID] = None
138
139
  task_parameters_id: Optional[UUID] = None
139
140
 
@@ -160,7 +161,9 @@ class State(ObjectBaseModel, Generic[R]):
160
161
  def result(self: "State[R]", raise_on_failure: bool = False) -> Union[R, Exception]:
161
162
  ...
162
163
 
163
- def result(self, raise_on_failure: bool = True, fetch: Optional[bool] = None):
164
+ def result(
165
+ self, raise_on_failure: bool = True, fetch: Optional[bool] = None
166
+ ) -> Union[R, Exception]:
164
167
  """
165
168
  Retrieve the result attached to this state.
166
169
 
prefect/engine.py CHANGED
@@ -163,6 +163,7 @@ from prefect.logging.loggers import (
163
163
  from prefect.results import ResultFactory, UnknownResult
164
164
  from prefect.settings import (
165
165
  PREFECT_DEBUG_MODE,
166
+ PREFECT_EXPERIMENTAL_ENABLE_NEW_ENGINE,
166
167
  PREFECT_TASK_INTROSPECTION_WARN_THRESHOLD,
167
168
  PREFECT_TASKS_REFRESH_CACHE,
168
169
  PREFECT_UI_URL,
@@ -189,6 +190,7 @@ from prefect.utilities.annotations import allow_failure, quote, unmapped
189
190
  from prefect.utilities.asyncutils import (
190
191
  gather,
191
192
  is_async_fn,
193
+ run_sync,
192
194
  sync_compatible,
193
195
  )
194
196
  from prefect.utilities.callables import (
@@ -2433,7 +2435,21 @@ if __name__ == "__main__":
2433
2435
  exit(1)
2434
2436
 
2435
2437
  try:
2436
- enter_flow_run_engine_from_subprocess(flow_run_id)
2438
+ if PREFECT_EXPERIMENTAL_ENABLE_NEW_ENGINE.value():
2439
+ from prefect.new_flow_engine import (
2440
+ load_flow_and_flow_run,
2441
+ run_flow,
2442
+ run_flow_sync,
2443
+ )
2444
+
2445
+ flow_run, flow = run_sync(load_flow_and_flow_run)
2446
+ # run the flow
2447
+ if flow.isasync:
2448
+ run_sync(run_flow(flow, flow_run=flow_run))
2449
+ else:
2450
+ run_flow_sync(flow, flow_run=flow_run)
2451
+ else:
2452
+ enter_flow_run_engine_from_subprocess(flow_run_id)
2437
2453
  except Abort as exc:
2438
2454
  engine_logger.info(
2439
2455
  f"Engine execution of flow run '{flow_run_id}' aborted by orchestrator:"
@@ -4,6 +4,7 @@ Command line interface for working with automations.
4
4
 
5
5
  import functools
6
6
  from typing import Optional
7
+ from uuid import UUID
7
8
 
8
9
  import orjson
9
10
  import typer
@@ -16,6 +17,8 @@ from prefect.cli._types import PrefectTyper
16
17
  from prefect.cli._utilities import exit_with_error, exit_with_success
17
18
  from prefect.cli.root import app
18
19
  from prefect.client.orchestration import get_client
20
+ from prefect.events.schemas.automations import Automation
21
+ from prefect.exceptions import PrefectHTTPStatusError
19
22
 
20
23
  automations_app = PrefectTyper(
21
24
  name="automation",
@@ -98,55 +101,175 @@ async def ls():
98
101
 
99
102
  @automations_app.command()
100
103
  @requires_automations
101
- async def inspect(id_or_name: str, yaml: bool = False, json: bool = False):
102
- """Inspect an automation."""
103
- async with get_client() as client:
104
- automation = await client.find_automation(id_or_name)
105
- if not automation:
106
- exit_with_error(f"Automation {id_or_name!r} not found.")
104
+ async def inspect(
105
+ name: Optional[str] = typer.Argument(None, help="An automation's name"),
106
+ id: Optional[str] = typer.Option(None, "--id", help="An automation's id"),
107
+ yaml: bool = typer.Option(False, "--yaml", help="Output as YAML"),
108
+ json: bool = typer.Option(False, "--json", help="Output as JSON"),
109
+ ):
110
+ """
111
+ Inspect an automation.
107
112
 
108
- if yaml:
109
- app.console.print(
110
- pyyaml.dump(automation.dict(json_compatible=True), sort_keys=False)
111
- )
112
- elif json:
113
- app.console.print(
114
- orjson.dumps(
115
- automation.dict(json_compatible=True), option=orjson.OPT_INDENT_2
116
- ).decode()
117
- )
113
+ Arguments:
114
+
115
+ name: the name of the automation to inspect
116
+
117
+ id: the id of the automation to inspect
118
+
119
+ yaml: output as YAML
120
+
121
+ json: output as JSON
122
+
123
+ Examples:
124
+
125
+ $ prefect automation inspect "my-automation"
126
+
127
+ $ prefect automation inspect --id "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
128
+
129
+ $ prefect automation inspect "my-automation" --yaml
130
+
131
+ $ prefect automation inspect "my-automation" --json
132
+ """
133
+ if not id and not name:
134
+ exit_with_error("Please provide either a name or an id.")
135
+
136
+ if name:
137
+ async with get_client() as client:
138
+ automation = await client.read_automations_by_name(name=name)
139
+ if not automation:
140
+ exit_with_error(f"Automation {name!r} not found.")
141
+
142
+ elif id:
143
+ async with get_client() as client:
144
+ try:
145
+ uuid_id = UUID(id)
146
+ automation = await client.read_automation(uuid_id)
147
+ except (PrefectHTTPStatusError, ValueError):
148
+ exit_with_error(f"Automation with id {id!r} not found.")
149
+
150
+ if yaml or json:
151
+ if isinstance(automation, list):
152
+ automation = [a.dict(json_compatible=True) for a in automation]
153
+ elif isinstance(automation, Automation):
154
+ automation = automation.dict(json_compatible=True)
155
+ if yaml:
156
+ app.console.print(pyyaml.dump(automation, sort_keys=False))
157
+ elif json:
158
+ app.console.print(
159
+ orjson.dumps(automation, option=orjson.OPT_INDENT_2).decode()
160
+ )
118
161
  else:
119
162
  app.console.print(Pretty(automation))
120
163
 
121
164
 
122
165
  @automations_app.command(aliases=["enable"])
123
166
  @requires_automations
124
- async def resume(id_or_name: str):
125
- """Resume an automation."""
126
- async with get_client() as client:
127
- automation = await client.find_automation(id_or_name)
128
- if not automation:
129
- exit_with_error(f"Automation {id_or_name!r} not found.")
167
+ async def resume(
168
+ name: Optional[str] = typer.Argument(None, help="An automation's name"),
169
+ id: Optional[str] = typer.Option(None, "--id", help="An automation's id"),
170
+ ):
171
+ """
172
+ Resume an automation.
130
173
 
131
- async with get_client() as client:
132
- await client.resume_automation(automation.id)
174
+ Arguments:
133
175
 
134
- exit_with_success(f"Resumed automation {automation.name!r} ({automation.id})")
176
+ name: the name of the automation to resume
177
+
178
+ id: the id of the automation to resume
179
+
180
+ Examples:
181
+
182
+ $ prefect automation resume "my-automation"
183
+
184
+ $ prefect automation resume --id "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
185
+ """
186
+ if not id and not name:
187
+ exit_with_error("Please provide either a name or an id.")
188
+
189
+ if name:
190
+ async with get_client() as client:
191
+ automation = await client.read_automations_by_name(name=name)
192
+ if not automation:
193
+ exit_with_error(
194
+ f"Automation with name {name!r} not found. You can also specify an id with the `--id` flag."
195
+ )
196
+ if len(automation) > 1:
197
+ if not typer.confirm(
198
+ f"Multiple automations found with name {name!r}. Do you want to resume all of them?",
199
+ default=False,
200
+ ):
201
+ exit_with_error("Resume aborted.")
202
+
203
+ for a in automation:
204
+ await client.resume_automation(a.id)
205
+ exit_with_success(
206
+ f"Resumed automation(s) with name {name!r} and id(s) {', '.join([repr(str(a.id)) for a in automation])}."
207
+ )
208
+
209
+ elif id:
210
+ async with get_client() as client:
211
+ try:
212
+ uuid_id = UUID(id)
213
+ automation = await client.read_automation(uuid_id)
214
+ except (PrefectHTTPStatusError, ValueError):
215
+ exit_with_error(f"Automation with id {id!r} not found.")
216
+ await client.resume_automation(automation.id)
217
+ exit_with_success(f"Resumed automation with id {str(automation.id)!r}.")
135
218
 
136
219
 
137
220
  @automations_app.command(aliases=["disable"])
138
221
  @requires_automations
139
- async def pause(id_or_name: str):
140
- """Pause an automation."""
141
- async with get_client() as client:
142
- automation = await client.find_automation(id_or_name)
143
- if not automation:
144
- exit_with_error(f"Automation {id_or_name!r} not found.")
222
+ async def pause(
223
+ name: Optional[str] = typer.Argument(None, help="An automation's name"),
224
+ id: Optional[str] = typer.Option(None, "--id", help="An automation's id"),
225
+ ):
226
+ """
227
+ Pause an automation.
145
228
 
146
- async with get_client() as client:
147
- await client.pause_automation(automation.id)
229
+ Arguments:
230
+
231
+ name: the name of the automation to pause
232
+
233
+ id: the id of the automation to pause
234
+
235
+ Examples:
236
+
237
+ $ prefect automation pause "my-automation"
238
+
239
+ $ prefect automation pause --id "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
240
+ """
241
+ if not id and not name:
242
+ exit_with_error("Please provide either a name or an id.")
243
+
244
+ if name:
245
+ async with get_client() as client:
246
+ automation = await client.read_automations_by_name(name=name)
247
+ if not automation:
248
+ exit_with_error(
249
+ f"Automation with name {name!r} not found. You can also specify an id with the `--id` flag."
250
+ )
251
+ if len(automation) > 1:
252
+ if not typer.confirm(
253
+ f"Multiple automations found with name {name!r}. Do you want to pause all of them?",
254
+ default=False,
255
+ ):
256
+ exit_with_error("Pause aborted.")
257
+
258
+ for a in automation:
259
+ await client.pause_automation(a.id)
260
+ exit_with_success(
261
+ f"Paused automation(s) with name {name!r} and id(s) {', '.join([repr(str(a.id)) for a in automation])}."
262
+ )
148
263
 
149
- exit_with_success(f"Paused automation {automation.name!r} ({automation.id})")
264
+ elif id:
265
+ async with get_client() as client:
266
+ try:
267
+ uuid_id = UUID(id)
268
+ automation = await client.read_automation(uuid_id)
269
+ except (PrefectHTTPStatusError, ValueError):
270
+ exit_with_error(f"Automation with id {id!r} not found.")
271
+ await client.pause_automation(automation.id)
272
+ exit_with_success(f"Paused automation with id {str(automation.id)!r}.")
150
273
 
151
274
 
152
275
  @automations_app.command()
prefect/events/clients.py CHANGED
@@ -11,6 +11,7 @@ from typing import (
11
11
  Optional,
12
12
  Tuple,
13
13
  Type,
14
+ cast,
14
15
  )
15
16
  from uuid import UUID
16
17
 
@@ -377,7 +378,7 @@ SEEN_EVENTS_TTL = 120
377
378
 
378
379
  class PrefectEventSubscriber:
379
380
  """
380
- Subscribes to a Prefect Cloud event stream, yielding events as they occur.
381
+ Subscribes to a Prefect event stream, yielding events as they occur.
381
382
 
382
383
  Example:
383
384
 
@@ -412,7 +413,7 @@ class PrefectEventSubscriber:
412
413
  the client should attempt to reconnect
413
414
  """
414
415
  if not api_url:
415
- api_url = PREFECT_API_URL.value()
416
+ api_url = cast(str, PREFECT_API_URL.value())
416
417
  self._api_key = None
417
418
 
418
419
  from prefect.events.filters import EventFilter
prefect/events/filters.py CHANGED
@@ -43,7 +43,7 @@ class AutomationFilter(PrefectBaseModel):
43
43
  )
44
44
 
45
45
 
46
- class EventDataFilter(PrefectBaseModel, extra="forbid"):
46
+ class EventDataFilter(PrefectBaseModel, extra="forbid"): # type: ignore[call-arg]
47
47
  """A base class for filtering event data."""
48
48
 
49
49
  _top_level_filter: "EventFilter | None" = PrivateAttr(None)
@@ -39,7 +39,7 @@ class Posture(AutoEnum):
39
39
  Metric = "Metric"
40
40
 
41
41
 
42
- class Trigger(PrefectBaseModel, abc.ABC, extra="ignore"):
42
+ class Trigger(PrefectBaseModel, abc.ABC, extra="ignore"): # type: ignore[call-arg]
43
43
  """
44
44
  Base class describing a set of criteria that must be satisfied in order to trigger
45
45
  an automation.
@@ -391,7 +391,7 @@ CompoundTrigger.update_forward_refs()
391
391
  SequenceTrigger.update_forward_refs()
392
392
 
393
393
 
394
- class AutomationCore(PrefectBaseModel, extra="ignore"):
394
+ class AutomationCore(PrefectBaseModel, extra="ignore"): # type: ignore[call-arg]
395
395
  """Defines an action a user wants to take when a certain number of events
396
396
  do or don't happen to the matching resources"""
397
397
 
@@ -48,7 +48,7 @@ from .automations import (
48
48
  from .events import ResourceSpecification
49
49
 
50
50
 
51
- class BaseDeploymentTrigger(PrefectBaseModel, abc.ABC, extra="ignore"):
51
+ class BaseDeploymentTrigger(PrefectBaseModel, abc.ABC, extra="ignore"): # type: ignore[call-arg]
52
52
  """
53
53
  Base class describing a set of criteria that must be satisfied in order to trigger
54
54
  an automation.
@@ -192,15 +192,22 @@ class ReceivedEvent(Event):
192
192
 
193
193
  def matches(expected: str, value: Optional[str]) -> bool:
194
194
  """Returns true if the given value matches the expected string, which may
195
- include wildcards"""
195
+ include a a negation prefix ("!this-value") or a wildcard suffix
196
+ ("any-value-starting-with*")"""
196
197
  if value is None:
197
198
  return False
198
199
 
199
- # TODO: handle wildcards/globs better than this
200
+ positive = True
201
+ if expected.startswith("!"):
202
+ expected = expected[1:]
203
+ positive = False
204
+
200
205
  if expected.endswith("*"):
201
- return value.startswith(expected[:-1])
206
+ match = value.startswith(expected[:-1])
207
+ else:
208
+ match = value == expected
202
209
 
203
- return value == expected
210
+ return match if positive else not match
204
211
 
205
212
 
206
213
  class ResourceSpecification(PrefectBaseModel):
@@ -71,7 +71,7 @@ class LabelDiver:
71
71
  raise AttributeError
72
72
 
73
73
 
74
- class Labelled(PrefectBaseModel, extra="ignore"):
74
+ class Labelled(PrefectBaseModel, extra="ignore"): # type: ignore[call-arg]
75
75
  """An object defined by string labels and values"""
76
76
 
77
77
  __root__: Dict[str, str]