prefect-client 3.2.1__py3-none-any.whl → 3.2.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.
Files changed (72) hide show
  1. prefect/__init__.py +15 -8
  2. prefect/_build_info.py +5 -0
  3. prefect/_internal/schemas/bases.py +4 -7
  4. prefect/_internal/schemas/validators.py +5 -6
  5. prefect/_result_records.py +6 -1
  6. prefect/client/orchestration/__init__.py +18 -6
  7. prefect/client/schemas/schedules.py +2 -2
  8. prefect/concurrency/asyncio.py +4 -3
  9. prefect/concurrency/sync.py +3 -3
  10. prefect/concurrency/v1/asyncio.py +3 -3
  11. prefect/concurrency/v1/sync.py +3 -3
  12. prefect/deployments/flow_runs.py +2 -2
  13. prefect/docker/docker_image.py +2 -3
  14. prefect/engine.py +1 -1
  15. prefect/events/clients.py +4 -3
  16. prefect/events/related.py +3 -5
  17. prefect/flows.py +11 -5
  18. prefect/locking/filesystem.py +8 -8
  19. prefect/logging/handlers.py +7 -11
  20. prefect/main.py +0 -2
  21. prefect/runtime/flow_run.py +10 -17
  22. prefect/server/api/__init__.py +34 -0
  23. prefect/server/api/admin.py +85 -0
  24. prefect/server/api/artifacts.py +224 -0
  25. prefect/server/api/automations.py +239 -0
  26. prefect/server/api/block_capabilities.py +25 -0
  27. prefect/server/api/block_documents.py +164 -0
  28. prefect/server/api/block_schemas.py +153 -0
  29. prefect/server/api/block_types.py +211 -0
  30. prefect/server/api/clients.py +246 -0
  31. prefect/server/api/collections.py +75 -0
  32. prefect/server/api/concurrency_limits.py +286 -0
  33. prefect/server/api/concurrency_limits_v2.py +269 -0
  34. prefect/server/api/csrf_token.py +38 -0
  35. prefect/server/api/dependencies.py +196 -0
  36. prefect/server/api/deployments.py +941 -0
  37. prefect/server/api/events.py +300 -0
  38. prefect/server/api/flow_run_notification_policies.py +120 -0
  39. prefect/server/api/flow_run_states.py +52 -0
  40. prefect/server/api/flow_runs.py +867 -0
  41. prefect/server/api/flows.py +210 -0
  42. prefect/server/api/logs.py +43 -0
  43. prefect/server/api/middleware.py +73 -0
  44. prefect/server/api/root.py +35 -0
  45. prefect/server/api/run_history.py +170 -0
  46. prefect/server/api/saved_searches.py +99 -0
  47. prefect/server/api/server.py +891 -0
  48. prefect/server/api/task_run_states.py +52 -0
  49. prefect/server/api/task_runs.py +342 -0
  50. prefect/server/api/task_workers.py +31 -0
  51. prefect/server/api/templates.py +35 -0
  52. prefect/server/api/ui/__init__.py +3 -0
  53. prefect/server/api/ui/flow_runs.py +128 -0
  54. prefect/server/api/ui/flows.py +173 -0
  55. prefect/server/api/ui/schemas.py +63 -0
  56. prefect/server/api/ui/task_runs.py +175 -0
  57. prefect/server/api/validation.py +382 -0
  58. prefect/server/api/variables.py +181 -0
  59. prefect/server/api/work_queues.py +230 -0
  60. prefect/server/api/workers.py +656 -0
  61. prefect/settings/sources.py +18 -5
  62. prefect/states.py +3 -3
  63. prefect/task_engine.py +3 -3
  64. prefect/types/_datetime.py +82 -3
  65. prefect/utilities/dockerutils.py +2 -2
  66. prefect/workers/base.py +5 -5
  67. {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info}/METADATA +10 -15
  68. {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info}/RECORD +70 -32
  69. {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info}/WHEEL +1 -2
  70. prefect/_version.py +0 -21
  71. prefect_client-3.2.1.dist-info/top_level.txt +0 -1
  72. {prefect_client-3.2.1.dist-info → prefect_client-3.2.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,382 @@
1
+ """
2
+ This module contains functions for validating job variables for deployments, work pools,
3
+ flow runs, and RunDeployment actions. These functions are used to validate that job
4
+ variables provided by users conform to the JSON schema defined in the work pool's base job
5
+ template.
6
+
7
+ Note some important details:
8
+
9
+ 1. The order of applying job variables is: work pool's base job template, deployment, flow
10
+ run. This means that flow run job variables override deployment job variables, which
11
+ override work pool job variables.
12
+
13
+ 2. The validation of job variables for work pools and deployments ignores required keys in
14
+ because we don't know if the full set of overrides will include values for any required
15
+ fields.
16
+
17
+ 3. Work pools can include default values for job variables. These can be normal types or
18
+ references to blocks. We have not been validating these values or whether default blocks
19
+ satisfy job variable JSON schemas. To avoid failing validation for existing (otherwise
20
+ working) data, we ignore invalid defaults when validating deployment and flow run
21
+ variables, but not when validating the work pool's base template, e.g. during work pool
22
+ creation or updates. If we find defaults that are invalid, we have to ignore required
23
+ fields when we run the full validation.
24
+
25
+ 4. A flow run is the terminal point for job variables, so it is the only place where
26
+ we validate required variables and default values. Thus,
27
+ `validate_job_variables_for_deployment_flow_run` and
28
+ `validate_job_variables_for_run_deployment_action` check for required fields.
29
+
30
+ 5. We have been using Pydantic v1 to generate work pool base job templates, and it produces
31
+ invalid JSON schemas for some fields, e.g. tuples and optional fields. We try to fix these
32
+ schemas on the fly while validating job variables, but there is a case we can't resolve,
33
+ which is whether or not an optional field supports a None value. In this case, we allow
34
+ None values to be passed in, which means that if an optional field does not actually
35
+ allow None values, the Pydantic model will fail to validate at runtime.
36
+ """
37
+
38
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Union
39
+ from uuid import UUID
40
+
41
+ import pydantic
42
+ from fastapi import HTTPException, status
43
+ from sqlalchemy.exc import DBAPIError, NoInspectionAvailable
44
+ from sqlalchemy.ext.asyncio import AsyncSession
45
+
46
+ from prefect.logging import get_logger
47
+ from prefect.server import models, schemas
48
+ from prefect.server.database.orm_models import Deployment as BaseDeployment
49
+ from prefect.server.events.actions import RunDeployment
50
+ from prefect.server.schemas.core import WorkPool
51
+ from prefect.utilities.schema_tools import ValidationError, is_valid_schema, validate
52
+
53
+ if TYPE_CHECKING:
54
+ import logging
55
+
56
+ logger: "logging.Logger" = get_logger("server.api.validation")
57
+
58
+ DeploymentAction = Union[
59
+ schemas.actions.DeploymentCreate, schemas.actions.DeploymentUpdate
60
+ ]
61
+ FlowRunAction = Union[
62
+ schemas.actions.DeploymentFlowRunCreate, schemas.actions.FlowRunUpdate
63
+ ]
64
+
65
+
66
+ async def _get_base_config_defaults(
67
+ session: AsyncSession,
68
+ base_config: dict[str, Any],
69
+ ignore_invalid_defaults: bool = True,
70
+ ) -> tuple[dict[str, Any], bool]:
71
+ variables_schema = base_config.get("variables", {})
72
+ fields_schema: dict[str, Any] = variables_schema.get("properties", {})
73
+ defaults: dict[str, Any] = dict()
74
+ has_invalid_defaults = False
75
+
76
+ if not fields_schema:
77
+ return defaults, has_invalid_defaults
78
+
79
+ for variable_name, attrs in fields_schema.items():
80
+ if "default" not in attrs:
81
+ continue
82
+
83
+ default = attrs["default"]
84
+
85
+ if isinstance(default, dict) and "$ref" in default:
86
+ hydrated_block = await _resolve_default_reference(default, session)
87
+ if hydrated_block is None:
88
+ continue
89
+ defaults[variable_name] = hydrated_block
90
+ else:
91
+ defaults[variable_name] = default
92
+
93
+ if ignore_invalid_defaults:
94
+ errors = validate(
95
+ {variable_name: defaults[variable_name]},
96
+ variables_schema,
97
+ raise_on_error=False,
98
+ preprocess=False,
99
+ ignore_required=True,
100
+ allow_none_with_default=False,
101
+ )
102
+ if errors:
103
+ has_invalid_defaults = True
104
+ try:
105
+ del defaults[variable_name]
106
+ except (IndexError, KeyError):
107
+ pass
108
+
109
+ return defaults, has_invalid_defaults
110
+
111
+
112
+ async def _resolve_default_reference(
113
+ variable: dict[str, Any], session: AsyncSession
114
+ ) -> Optional[Any]:
115
+ """
116
+ Resolve a reference to a block. The input variable should have a format of:
117
+
118
+ {
119
+ "$ref": {
120
+ "block_document_id": "block_document_id"
121
+ },
122
+ }
123
+ """
124
+ if not isinstance(variable, dict):
125
+ return None
126
+
127
+ if "$ref" not in variable:
128
+ return None
129
+
130
+ reference_data = variable.get("$ref", {})
131
+ if (provided_block_document_id := reference_data.get("block_document_id")) is None:
132
+ return None
133
+
134
+ if isinstance(provided_block_document_id, UUID):
135
+ block_document_id = provided_block_document_id
136
+ else:
137
+ try:
138
+ block_document_id = UUID(provided_block_document_id)
139
+ except ValueError:
140
+ raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Block not found.")
141
+
142
+ try:
143
+ block_document = await models.block_documents.read_block_document_by_id(
144
+ session, block_document_id
145
+ )
146
+ except pydantic.ValidationError:
147
+ # It's possible to get an invalid UUID here because the block document ID is
148
+ # not validated by our schemas.
149
+ logger.info("Could not find block document with ID %s", block_document_id)
150
+ block_document = None
151
+
152
+ if not block_document:
153
+ raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Block not found.")
154
+
155
+ return block_document.data
156
+
157
+
158
+ async def _validate_work_pool_job_variables(
159
+ session: AsyncSession,
160
+ work_pool_name: str,
161
+ base_job_template: Dict[str, Any],
162
+ *job_vars: Dict[str, Any],
163
+ ignore_required: bool = True,
164
+ ignore_invalid_defaults: bool = True,
165
+ raise_on_error=True,
166
+ ) -> None:
167
+ if not base_job_template:
168
+ logger.info(
169
+ "Cannot validate job variables for work pool %s because it does not have a base job template",
170
+ work_pool_name,
171
+ )
172
+ return
173
+
174
+ variables_schema = base_job_template.get("variables")
175
+ if not variables_schema:
176
+ logger.info(
177
+ "Cannot validate job variables for work pool %s "
178
+ "because it does not specify a variables schema",
179
+ work_pool_name,
180
+ )
181
+ return
182
+
183
+ try:
184
+ is_valid_schema(variables_schema, preprocess=False)
185
+ except ValueError as exc:
186
+ raise HTTPException(
187
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)
188
+ )
189
+
190
+ base_vars, invalid_defaults = await _get_base_config_defaults(
191
+ session, base_job_template, ignore_invalid_defaults
192
+ )
193
+ all_job_vars = {**base_vars}
194
+
195
+ for jvs in job_vars:
196
+ if isinstance(jvs, dict):
197
+ all_job_vars.update(jvs)
198
+
199
+ # If we are ignoring validation for default values and there were invalid defaults,
200
+ # then we can't check for required fields because we won't have the default values
201
+ # to satisfy nissing required fields.
202
+ should_ignore_required = ignore_required or (
203
+ ignore_invalid_defaults and invalid_defaults
204
+ )
205
+
206
+ validate(
207
+ all_job_vars,
208
+ variables_schema,
209
+ raise_on_error=raise_on_error,
210
+ preprocess=True,
211
+ ignore_required=should_ignore_required,
212
+ # We allow None values to be passed in for optional fields if there is a default
213
+ # value for the field. This is because we have blocks that contain default None
214
+ # values that will fail to validate otherwise. However, this means that if an
215
+ # optional field does not actually allow None values, the Pydantic model will fail
216
+ # to validate at runtime. Unfortunately, there is not a good solution to this
217
+ # problem at this time.
218
+ allow_none_with_default=True,
219
+ )
220
+
221
+
222
+ async def validate_job_variables_for_deployment_flow_run(
223
+ session: AsyncSession,
224
+ deployment: BaseDeployment,
225
+ flow_run: FlowRunAction,
226
+ ) -> None:
227
+ """
228
+ Validate job variables for a flow run created for a deployment.
229
+
230
+ Flow runs are the terminal point for job variable overlays, so we validate required
231
+ job variables because all variables should now be present.
232
+ """
233
+ # If we aren't able to access a deployment's work pool, we don't have a base job
234
+ # template to validate job variables against. This is not a validation failure because
235
+ # some deployments may not have a work pool, such as those created by flow.serve().
236
+ if not (deployment.work_queue and deployment.work_queue.work_pool):
237
+ logger.info(
238
+ "Cannot validate job variables for deployment %s "
239
+ "because it does not have a work pool",
240
+ deployment.id,
241
+ )
242
+ return
243
+
244
+ work_pool = deployment.work_queue.work_pool
245
+
246
+ try:
247
+ await _validate_work_pool_job_variables(
248
+ session,
249
+ work_pool.name,
250
+ work_pool.base_job_template,
251
+ deployment.job_variables or {},
252
+ flow_run.job_variables or {},
253
+ ignore_required=False,
254
+ ignore_invalid_defaults=True,
255
+ )
256
+ except ValidationError as exc:
257
+ if isinstance(flow_run, schemas.actions.DeploymentFlowRunCreate):
258
+ error_msg = f"Error creating flow run: {exc}"
259
+ else:
260
+ error_msg = f"Error updating flow run: {exc}"
261
+ raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=error_msg)
262
+
263
+
264
+ async def validate_job_variables_for_deployment(
265
+ session: AsyncSession,
266
+ work_pool: WorkPool,
267
+ deployment: DeploymentAction,
268
+ ) -> None:
269
+ """
270
+ Validate job variables for deployment creation and updates.
271
+
272
+ This validation applies only to deployments that have a work pool. If the deployment
273
+ does not have a work pool, we cannot validate job variables because we don't have a
274
+ base job template to validate against, so we skip this validation.
275
+
276
+ Unlike validations for flow runs, validation here ignores required keys in the schema
277
+ because we don't know if the full set of overrides will include values for any
278
+ required fields. If the full set of job variables when a flow is running, including
279
+ the deployment's and flow run's overrides, fails to specify a value for the required
280
+ key, that's an error.
281
+ """
282
+ if not deployment.job_variables:
283
+ return
284
+ try:
285
+ await _validate_work_pool_job_variables(
286
+ session,
287
+ work_pool.name,
288
+ work_pool.base_job_template,
289
+ deployment.job_variables or {},
290
+ ignore_required=True,
291
+ ignore_invalid_defaults=True,
292
+ )
293
+ except ValidationError as exc:
294
+ if isinstance(deployment, schemas.actions.DeploymentCreate):
295
+ error_msg = f"Error creating deployment: {exc}"
296
+ else:
297
+ error_msg = f"Error updating deployment: {exc}"
298
+ raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=error_msg)
299
+
300
+
301
+ async def validate_job_variable_defaults_for_work_pool(
302
+ session: AsyncSession,
303
+ work_pool_name: str,
304
+ base_job_template: Dict[str, Any],
305
+ ) -> None:
306
+ """
307
+ Validate the default job variables for a work pool.
308
+
309
+ This validation checks that default values for job variables match the JSON schema
310
+ defined in the work pool's base job template. It also resolves references to block
311
+ documents in the default values and hydrates them to perform the validation.
312
+
313
+ Unlike validations for flow runs, validation here ignores required keys in the schema
314
+ because we're only concerned with default values. The absence of a default for a
315
+ required field is not an error, but if the full set of job variables when a flow is
316
+ running, including the deployment's and flow run's overrides, fails to specify a value
317
+ for the required key, that's an error.
318
+
319
+ NOTE: This will raise an HTTP 404 error if a referenced block document does not exist.
320
+ """
321
+ try:
322
+ await _validate_work_pool_job_variables(
323
+ session,
324
+ work_pool_name,
325
+ base_job_template,
326
+ ignore_required=True,
327
+ ignore_invalid_defaults=False,
328
+ )
329
+ except ValidationError as exc:
330
+ error_msg = f"Validation failed for work pool's job variable defaults: {exc}"
331
+ raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=error_msg)
332
+
333
+
334
+ async def validate_job_variables_for_run_deployment_action(
335
+ session: AsyncSession,
336
+ run_action: RunDeployment,
337
+ ) -> None:
338
+ """
339
+ Validate the job variables for a RunDeployment action.
340
+
341
+ This action is equivalent to creating a flow run for a deployment, so we validate
342
+ required job variables because all variables should now be present.
343
+ """
344
+ if not run_action.deployment_id:
345
+ logger.error(
346
+ "Cannot validate job variables for RunDeployment action because it does not have a deployment ID"
347
+ )
348
+ return
349
+
350
+ try:
351
+ deployment = await models.deployments.read_deployment(
352
+ session, run_action.deployment_id
353
+ )
354
+ except (DBAPIError, NoInspectionAvailable):
355
+ # It's possible to get an invalid UUID here because the deployment ID is
356
+ # not validated by our schemas.
357
+ logger.info("Could not find deployment with ID %s", run_action.deployment_id)
358
+ deployment = None
359
+ if not deployment:
360
+ raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Deployment not found.")
361
+
362
+ if not (deployment.work_queue and deployment.work_queue.work_pool):
363
+ logger.info(
364
+ "Cannot validate job variables for deployment %s "
365
+ "because it does not have a work pool",
366
+ run_action.deployment_id,
367
+ )
368
+ return
369
+
370
+ if not (deployment.job_variables or run_action.job_variables):
371
+ return
372
+
373
+ work_pool = deployment.work_queue.work_pool
374
+
375
+ await _validate_work_pool_job_variables(
376
+ session,
377
+ work_pool.name,
378
+ work_pool.base_job_template,
379
+ run_action.job_variables or {},
380
+ ignore_required=False,
381
+ ignore_invalid_defaults=True,
382
+ )
@@ -0,0 +1,181 @@
1
+ """
2
+ Routes for interacting with variable objects
3
+ """
4
+
5
+ from typing import List, Optional
6
+ from uuid import UUID
7
+
8
+ import sqlalchemy as sa
9
+ from fastapi import Body, Depends, HTTPException, Path, status
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ from prefect.server import models
13
+ from prefect.server.api.dependencies import LimitBody
14
+ from prefect.server.database import (
15
+ PrefectDBInterface,
16
+ orm_models,
17
+ provide_database_interface,
18
+ )
19
+ from prefect.server.schemas import actions, core, filters, sorting
20
+ from prefect.server.utilities.server import PrefectRouter
21
+
22
+
23
+ async def get_variable_or_404(
24
+ session: AsyncSession, variable_id: UUID
25
+ ) -> orm_models.Variable:
26
+ """Returns a variable or raises 404 HTTPException if it does not exist"""
27
+
28
+ variable = await models.variables.read_variable(
29
+ session=session, variable_id=variable_id
30
+ )
31
+ if not variable:
32
+ raise HTTPException(status_code=404, detail="Variable not found.")
33
+
34
+ return variable
35
+
36
+
37
+ async def get_variable_by_name_or_404(
38
+ session: AsyncSession, name: str
39
+ ) -> orm_models.Variable:
40
+ """Returns a variable or raises 404 HTTPException if it does not exist"""
41
+
42
+ variable = await models.variables.read_variable_by_name(session=session, name=name)
43
+ if not variable:
44
+ raise HTTPException(status_code=404, detail="Variable not found.")
45
+
46
+ return variable
47
+
48
+
49
+ router: PrefectRouter = PrefectRouter(
50
+ prefix="/variables",
51
+ tags=["Variables"],
52
+ )
53
+
54
+
55
+ @router.post("/", status_code=status.HTTP_201_CREATED)
56
+ async def create_variable(
57
+ variable: actions.VariableCreate,
58
+ db: PrefectDBInterface = Depends(provide_database_interface),
59
+ ) -> core.Variable:
60
+ async with db.session_context(begin_transaction=True) as session:
61
+ try:
62
+ model = await models.variables.create_variable(
63
+ session=session, variable=variable
64
+ )
65
+ except sa.exc.IntegrityError:
66
+ raise HTTPException(
67
+ status_code=409,
68
+ detail=f"A variable with the name {variable.name!r} already exists.",
69
+ )
70
+
71
+ return core.Variable.model_validate(model, from_attributes=True)
72
+
73
+
74
+ @router.get("/{id:uuid}")
75
+ async def read_variable(
76
+ variable_id: UUID = Path(..., alias="id"),
77
+ db: PrefectDBInterface = Depends(provide_database_interface),
78
+ ) -> core.Variable:
79
+ async with db.session_context() as session:
80
+ model = await get_variable_or_404(session=session, variable_id=variable_id)
81
+
82
+ return core.Variable.model_validate(model, from_attributes=True)
83
+
84
+
85
+ @router.get("/name/{name:str}")
86
+ async def read_variable_by_name(
87
+ name: str = Path(...),
88
+ db: PrefectDBInterface = Depends(provide_database_interface),
89
+ ) -> core.Variable:
90
+ async with db.session_context() as session:
91
+ model = await get_variable_by_name_or_404(session=session, name=name)
92
+
93
+ return core.Variable.model_validate(model, from_attributes=True)
94
+
95
+
96
+ @router.post("/filter")
97
+ async def read_variables(
98
+ limit: int = LimitBody(),
99
+ offset: int = Body(0, ge=0),
100
+ variables: Optional[filters.VariableFilter] = None,
101
+ sort: sorting.VariableSort = Body(sorting.VariableSort.NAME_ASC),
102
+ db: PrefectDBInterface = Depends(provide_database_interface),
103
+ ) -> List[core.Variable]:
104
+ async with db.session_context() as session:
105
+ return await models.variables.read_variables(
106
+ session=session,
107
+ variable_filter=variables,
108
+ sort=sort,
109
+ offset=offset,
110
+ limit=limit,
111
+ )
112
+
113
+
114
+ @router.post("/count")
115
+ async def count_variables(
116
+ variables: Optional[filters.VariableFilter] = Body(None, embed=True),
117
+ db: PrefectDBInterface = Depends(provide_database_interface),
118
+ ) -> int:
119
+ async with db.session_context() as session:
120
+ return await models.variables.count_variables(
121
+ session=session,
122
+ variable_filter=variables,
123
+ )
124
+
125
+
126
+ @router.patch("/{id:uuid}", status_code=status.HTTP_204_NO_CONTENT)
127
+ async def update_variable(
128
+ variable: actions.VariableUpdate,
129
+ variable_id: UUID = Path(..., alias="id"),
130
+ db: PrefectDBInterface = Depends(provide_database_interface),
131
+ ) -> None:
132
+ async with db.session_context(begin_transaction=True) as session:
133
+ updated = await models.variables.update_variable(
134
+ session=session,
135
+ variable_id=variable_id,
136
+ variable=variable,
137
+ )
138
+ if not updated:
139
+ raise HTTPException(status_code=404, detail="Variable not found.")
140
+
141
+
142
+ @router.patch("/name/{name:str}", status_code=status.HTTP_204_NO_CONTENT)
143
+ async def update_variable_by_name(
144
+ variable: actions.VariableUpdate,
145
+ name: str = Path(..., alias="name"),
146
+ db: PrefectDBInterface = Depends(provide_database_interface),
147
+ ) -> None:
148
+ async with db.session_context(begin_transaction=True) as session:
149
+ updated = await models.variables.update_variable_by_name(
150
+ session=session,
151
+ name=name,
152
+ variable=variable,
153
+ )
154
+ if not updated:
155
+ raise HTTPException(status_code=404, detail="Variable not found.")
156
+
157
+
158
+ @router.delete("/{id:uuid}", status_code=status.HTTP_204_NO_CONTENT)
159
+ async def delete_variable(
160
+ variable_id: UUID = Path(..., alias="id"),
161
+ db: PrefectDBInterface = Depends(provide_database_interface),
162
+ ) -> None:
163
+ async with db.session_context(begin_transaction=True) as session:
164
+ deleted = await models.variables.delete_variable(
165
+ session=session, variable_id=variable_id
166
+ )
167
+ if not deleted:
168
+ raise HTTPException(status_code=404, detail="Variable not found.")
169
+
170
+
171
+ @router.delete("/name/{name:str}", status_code=status.HTTP_204_NO_CONTENT)
172
+ async def delete_variable_by_name(
173
+ name: str = Path(...),
174
+ db: PrefectDBInterface = Depends(provide_database_interface),
175
+ ) -> None:
176
+ async with db.session_context(begin_transaction=True) as session:
177
+ deleted = await models.variables.delete_variable_by_name(
178
+ session=session, name=name
179
+ )
180
+ if not deleted:
181
+ raise HTTPException(status_code=404, detail="Variable not found.")