apache-airflow-providers-edge3 1.4.1rc2__py3-none-any.whl → 2.0.0__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 (50) hide show
  1. airflow/providers/edge3/__init__.py +3 -3
  2. airflow/providers/edge3/cli/api_client.py +23 -26
  3. airflow/providers/edge3/cli/worker.py +14 -29
  4. airflow/providers/edge3/example_dags/integration_test.py +1 -1
  5. airflow/providers/edge3/example_dags/win_test.py +32 -22
  6. airflow/providers/edge3/executors/edge_executor.py +7 -63
  7. airflow/providers/edge3/get_provider_info.py +7 -0
  8. airflow/providers/edge3/models/edge_worker.py +7 -3
  9. airflow/providers/edge3/plugins/edge_executor_plugin.py +26 -205
  10. airflow/providers/edge3/plugins/www/dist/main.umd.cjs +8 -100
  11. airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts +6 -1
  12. airflow/providers/edge3/plugins/www/openapi-gen/queries/ensureQueryData.ts +6 -1
  13. airflow/providers/edge3/plugins/www/openapi-gen/queries/prefetch.ts +6 -1
  14. airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts +6 -2
  15. airflow/providers/edge3/plugins/www/openapi-gen/queries/suspense.ts +6 -1
  16. airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts +5 -0
  17. airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts +18 -3
  18. airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts +24 -0
  19. airflow/providers/edge3/plugins/www/package.json +26 -24
  20. airflow/providers/edge3/plugins/www/pnpm-lock.yaml +1469 -1413
  21. airflow/providers/edge3/plugins/www/src/components/SearchBar.tsx +103 -0
  22. airflow/providers/edge3/plugins/www/src/components/ui/InputGroup.tsx +57 -0
  23. airflow/providers/edge3/plugins/www/src/components/ui/Select/Content.tsx +37 -0
  24. airflow/providers/edge3/plugins/www/src/components/ui/Select/Item.tsx +34 -0
  25. airflow/providers/edge3/plugins/www/src/components/ui/Select/Root.tsx +24 -0
  26. airflow/providers/edge3/plugins/www/src/components/ui/Select/Trigger.tsx +54 -0
  27. airflow/providers/edge3/plugins/www/src/components/ui/Select/ValueText.tsx +51 -0
  28. airflow/providers/edge3/plugins/www/src/components/ui/Select/index.ts +34 -0
  29. airflow/providers/edge3/plugins/www/src/components/ui/index.ts +3 -0
  30. airflow/providers/edge3/plugins/www/src/constants.ts +43 -0
  31. airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx +184 -95
  32. airflow/providers/edge3/version_compat.py +0 -2
  33. airflow/providers/edge3/worker_api/auth.py +11 -35
  34. airflow/providers/edge3/worker_api/datamodels.py +3 -2
  35. airflow/providers/edge3/worker_api/routes/health.py +1 -1
  36. airflow/providers/edge3/worker_api/routes/jobs.py +10 -11
  37. airflow/providers/edge3/worker_api/routes/logs.py +5 -8
  38. airflow/providers/edge3/worker_api/routes/ui.py +14 -3
  39. airflow/providers/edge3/worker_api/routes/worker.py +19 -12
  40. airflow/providers/edge3/{openapi → worker_api}/v2-edge-generated.yaml +59 -5
  41. {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/METADATA +16 -14
  42. {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/RECORD +46 -40
  43. apache_airflow_providers_edge3-2.0.0.dist-info/licenses/NOTICE +5 -0
  44. airflow/providers/edge3/openapi/__init__.py +0 -19
  45. airflow/providers/edge3/openapi/edge_worker_api_v1.yaml +0 -808
  46. airflow/providers/edge3/worker_api/routes/_v2_compat.py +0 -136
  47. airflow/providers/edge3/worker_api/routes/_v2_routes.py +0 -237
  48. {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/WHEEL +0 -0
  49. {apache_airflow_providers_edge3-1.4.1rc2.dist-info → apache_airflow_providers_edge3-2.0.0.dist-info}/entry_points.txt +0 -0
  50. {airflow/providers/edge3 → apache_airflow_providers_edge3-2.0.0.dist-info/licenses}/LICENSE +0 -0
@@ -29,11 +29,11 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "1.4.1"
32
+ __version__ = "2.0.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
- "2.10.0"
35
+ "3.0.0"
36
36
  ):
37
37
  raise RuntimeError(
38
- f"The package `apache-airflow-providers-edge3:{__version__}` needs Apache Airflow 2.10.0+"
38
+ f"The package `apache-airflow-providers-edge3:{__version__}` needs Apache Airflow 3.0.0+"
39
39
  )
@@ -20,6 +20,7 @@ import json
20
20
  import logging
21
21
  import os
22
22
  from datetime import datetime
23
+ from functools import cache
23
24
  from http import HTTPStatus
24
25
  from pathlib import Path
25
26
  from typing import TYPE_CHECKING, Any
@@ -27,11 +28,14 @@ from urllib.parse import quote, urljoin
27
28
 
28
29
  import requests
29
30
  from retryhttp import retry, wait_retry_after
30
- from tenacity import before_log, wait_random_exponential
31
+ from tenacity import before_sleep_log, wait_random_exponential
31
32
 
33
+ from airflow.api_fastapi.auth.tokens import JWTGenerator
32
34
  from airflow.configuration import conf
33
- from airflow.providers.edge3.models.edge_worker import EdgeWorkerVersionException
34
- from airflow.providers.edge3.version_compat import AIRFLOW_V_3_0_PLUS
35
+ from airflow.providers.edge3.models.edge_worker import (
36
+ EdgeWorkerDuplicateException,
37
+ EdgeWorkerVersionException,
38
+ )
35
39
  from airflow.providers.edge3.worker_api.datamodels import (
36
40
  EdgeJobFetched,
37
41
  PushLogsBody,
@@ -71,6 +75,15 @@ API_RETRY_WAIT_MAX = float(
71
75
  _default_wait = wait_random_exponential(min=API_RETRY_WAIT_MIN, max=API_RETRY_WAIT_MAX)
72
76
 
73
77
 
78
+ @cache
79
+ def jwt_generator() -> JWTGenerator:
80
+ return JWTGenerator(
81
+ secret_key=conf.get("api_auth", "jwt_secret"),
82
+ valid_for=conf.getint("api_auth", "jwt_leeway", fallback=30),
83
+ audience="api",
84
+ )
85
+
86
+
74
87
  @retry(
75
88
  reraise=True,
76
89
  max_attempt_number=API_RETRIES,
@@ -78,31 +91,10 @@ _default_wait = wait_random_exponential(min=API_RETRY_WAIT_MIN, max=API_RETRY_WA
78
91
  wait_network_errors=_default_wait,
79
92
  wait_timeouts=_default_wait,
80
93
  wait_rate_limited=wait_retry_after(fallback=_default_wait), # No infinite timeout on HTTP 429
81
- before_sleep=before_log(logger, logging.WARNING),
94
+ before_sleep=before_sleep_log(logger, logging.WARNING),
82
95
  )
83
96
  def _make_generic_request(method: str, rest_path: str, data: str | None = None) -> Any:
84
- if AIRFLOW_V_3_0_PLUS:
85
- from functools import cache
86
-
87
- from airflow.api_fastapi.auth.tokens import JWTGenerator
88
-
89
- @cache
90
- def jwt_generator() -> JWTGenerator:
91
- return JWTGenerator(
92
- secret_key=conf.get("api_auth", "jwt_secret"),
93
- valid_for=conf.getint("api_auth", "jwt_leeway", fallback=30),
94
- audience="api",
95
- )
96
-
97
- generator = jwt_generator()
98
- authorization = generator.generate({"method": rest_path})
99
- else:
100
- # Airflow 2.10 compatibility
101
- from airflow.providers.edge3.worker_api.auth import jwt_signer
102
-
103
- signer = jwt_signer()
104
- authorization = signer.generate_signed_token({"method": rest_path})
105
-
97
+ authorization = jwt_generator().generate({"method": rest_path})
106
98
  api_url = conf.get("edge", "api_url")
107
99
  headers = {
108
100
  "Content-Type": "application/json",
@@ -132,6 +124,11 @@ def worker_register(
132
124
  except requests.HTTPError as e:
133
125
  if e.response.status_code == 400:
134
126
  raise EdgeWorkerVersionException(str(e))
127
+ if e.response.status_code == 409:
128
+ raise EdgeWorkerDuplicateException(
129
+ f"A worker with the name '{hostname}' is already active. "
130
+ "Please ensure worker names are unique, or stop the existing worker before starting a new one."
131
+ )
135
132
  raise e
136
133
  return WorkerRegistrationReturn(**result)
137
134
 
@@ -25,7 +25,6 @@ from functools import cache
25
25
  from http import HTTPStatus
26
26
  from multiprocessing import Process
27
27
  from pathlib import Path
28
- from subprocess import Popen
29
28
  from time import sleep
30
29
  from typing import TYPE_CHECKING
31
30
 
@@ -39,7 +38,6 @@ from airflow.providers.edge3 import __version__ as edge_provider_version
39
38
  from airflow.providers.edge3.cli.api_client import (
40
39
  jobs_fetch,
41
40
  jobs_set_state,
42
- logs_logfile_path,
43
41
  logs_push,
44
42
  worker_register,
45
43
  worker_set_state,
@@ -51,8 +49,11 @@ from airflow.providers.edge3.cli.signalling import (
51
49
  status_file_path,
52
50
  write_pid_to_pidfile,
53
51
  )
54
- from airflow.providers.edge3.models.edge_worker import EdgeWorkerState, EdgeWorkerVersionException
55
- from airflow.providers.edge3.version_compat import AIRFLOW_V_3_0_PLUS
52
+ from airflow.providers.edge3.models.edge_worker import (
53
+ EdgeWorkerDuplicateException,
54
+ EdgeWorkerState,
55
+ EdgeWorkerVersionException,
56
+ )
56
57
  from airflow.utils.net import getfqdn
57
58
  from airflow.utils.state import TaskInstanceState
58
59
 
@@ -214,7 +215,7 @@ class EdgeWorker:
214
215
  return 1
215
216
 
216
217
  @staticmethod
217
- def _launch_job_af3(edge_job: EdgeJobFetched) -> tuple[Process, Path]:
218
+ def _launch_job(edge_job: EdgeJobFetched):
218
219
  if TYPE_CHECKING:
219
220
  from airflow.executors.workloads import ExecuteTask
220
221
 
@@ -228,29 +229,6 @@ class EdgeWorker:
228
229
  if TYPE_CHECKING:
229
230
  assert workload.log_path # We need to assume this is defined in here
230
231
  logfile = Path(base_log_folder, workload.log_path)
231
- return process, logfile
232
-
233
- @staticmethod
234
- def _launch_job_af2_10(edge_job: EdgeJobFetched) -> tuple[Popen, Path]:
235
- """Compatibility for Airflow 2.10 Launch."""
236
- env = os.environ.copy()
237
- env["AIRFLOW__CORE__DATABASE_ACCESS_ISOLATION"] = "True"
238
- env["AIRFLOW__CORE__INTERNAL_API_URL"] = conf.get("edge", "api_url")
239
- env["_AIRFLOW__SKIP_DATABASE_EXECUTOR_COMPATIBILITY_CHECK"] = "1"
240
- command: list[str] = edge_job.command # type: ignore[assignment]
241
- process = Popen(command, close_fds=True, env=env, start_new_session=True)
242
- logfile = logs_logfile_path(edge_job.key)
243
- return process, logfile
244
-
245
- @staticmethod
246
- def _launch_job(edge_job: EdgeJobFetched):
247
- """Get the received job executed."""
248
- process: Popen | Process
249
- if AIRFLOW_V_3_0_PLUS:
250
- process, logfile = EdgeWorker._launch_job_af3(edge_job)
251
- else:
252
- # Airflow 2.10
253
- process, logfile = EdgeWorker._launch_job_af2_10(edge_job)
254
232
  EdgeWorker.jobs.append(Job(edge_job, process, logfile, 0))
255
233
 
256
234
  def start(self):
@@ -262,6 +240,9 @@ class EdgeWorker:
262
240
  except EdgeWorkerVersionException as e:
263
241
  logger.info("Version mismatch of Edge worker and Core. Shutting down worker.")
264
242
  raise SystemExit(str(e))
243
+ except EdgeWorkerDuplicateException as e:
244
+ logger.error(str(e))
245
+ raise SystemExit(str(e))
265
246
  except HTTPError as e:
266
247
  if e.response.status_code == HTTPStatus.NOT_FOUND:
267
248
  raise SystemExit("Error: API endpoint is not ready, please set [edge] api_enabled=True.")
@@ -348,7 +329,11 @@ class EdgeWorker:
348
329
  else:
349
330
  used_concurrency += job.edge_job.concurrency_slots
350
331
 
351
- if job.logfile.exists() and job.logfile.stat().st_size > job.logsize:
332
+ if (
333
+ conf.getboolean("edge", "push_logs")
334
+ and job.logfile.exists()
335
+ and job.logfile.stat().st_size > job.logsize
336
+ ):
352
337
  with job.logfile.open("rb") as logfile:
353
338
  push_log_chunk_size = conf.getint("edge", "push_log_chunk_size")
354
339
  logfile.seek(job.logsize, os.SEEK_SET)
@@ -26,7 +26,7 @@ from __future__ import annotations
26
26
  from datetime import datetime
27
27
  from time import sleep
28
28
 
29
- from airflow.exceptions import AirflowNotFoundException
29
+ from airflow.providers.common.compat.sdk import AirflowNotFoundException
30
30
 
31
31
  try:
32
32
  from airflow.sdk import BaseHook
@@ -32,42 +32,52 @@ from subprocess import STDOUT, Popen
32
32
  from time import sleep
33
33
  from typing import TYPE_CHECKING, Any
34
34
 
35
- try:
36
- from airflow.sdk import task, task_group
37
- except ImportError:
38
- # Airflow 2 path
39
- from airflow.decorators import task, task_group # type: ignore[attr-defined,no-redef]
40
- from airflow.exceptions import AirflowException, AirflowNotFoundException, AirflowSkipException
41
35
  from airflow.models import BaseOperator
42
36
  from airflow.models.dag import DAG
43
37
  from airflow.models.variable import Variable
38
+ from airflow.providers.common.compat.sdk import (
39
+ AirflowException,
40
+ AirflowNotFoundException,
41
+ AirflowSkipException,
42
+ )
44
43
  from airflow.providers.standard.operators.empty import EmptyOperator
44
+ from airflow.sdk.execution_time.context import context_to_airflow_vars
45
45
 
46
+ try:
47
+ from airflow.sdk import task, task_group
48
+ except ImportError:
49
+ from airflow.decorators import task, task_group # type: ignore[attr-defined,no-redef]
46
50
  try:
47
51
  from airflow.sdk import BaseHook
48
52
  except ImportError:
49
53
  from airflow.hooks.base import BaseHook # type: ignore[attr-defined,no-redef]
50
- from airflow.sdk import Param
51
-
54
+ try:
55
+ from airflow.sdk import Param
56
+ except ImportError:
57
+ from airflow.models import Param # type: ignore[attr-defined,no-redef]
52
58
  try:
53
59
  from airflow.sdk import TriggerRule
54
60
  except ImportError:
55
- # Compatibility for Airflow < 3.1
56
61
  from airflow.utils.trigger_rule import TriggerRule # type: ignore[no-redef,attr-defined]
57
- from airflow.sdk.execution_time.context import context_to_airflow_vars
58
- from airflow.utils.types import ArgNotSet
59
-
60
- if TYPE_CHECKING:
61
- try:
62
- from airflow.sdk.types import RuntimeTaskInstanceProtocol as TaskInstance
63
- except ImportError:
64
- from airflow.models import TaskInstance # type: ignore[assignment]
65
- from airflow.utils.context import Context
66
-
67
62
  try:
68
- from airflow.operators.python import PythonOperator
63
+ from airflow.providers.common.compat.standard.operators import PythonOperator
64
+ except ImportError:
65
+ from airflow.operators.python import PythonOperator # type: ignore[no-redef]
66
+ try:
67
+ from airflow.sdk.definitions._internal.types import NOTSET, ArgNotSet
69
68
  except ImportError:
70
- from airflow.providers.common.compat.standard.operators import PythonOperator # type: ignore[no-redef]
69
+ from airflow.utils.types import NOTSET, ArgNotSet # type: ignore[attr-defined,no-redef]
70
+ try:
71
+ from airflow.sdk.definitions._internal.types import is_arg_set
72
+ except ImportError:
73
+
74
+ def is_arg_set(value): # type: ignore[misc,no-redef]
75
+ return value is not NOTSET
76
+
77
+
78
+ if TYPE_CHECKING:
79
+ from airflow.sdk import Context
80
+ from airflow.sdk.types import RuntimeTaskInstanceProtocol as TaskInstance
71
81
 
72
82
 
73
83
  class CmdOperator(BaseOperator):
@@ -163,7 +173,7 @@ class CmdOperator(BaseOperator):
163
173
  # When using the @task.command decorator, the command is not known until the underlying Python
164
174
  # callable is executed and therefore set to NOTSET initially. This flag is useful during execution to
165
175
  # determine whether the command value needs to re-rendered.
166
- self._init_command_not_set = isinstance(self.command, ArgNotSet)
176
+ self._init_command_not_set = not is_arg_set(self.command)
167
177
 
168
178
  @staticmethod
169
179
  def refresh_command(ti: TaskInstance) -> None:
@@ -29,15 +29,14 @@ from sqlalchemy.orm import Session
29
29
 
30
30
  from airflow.cli.cli_config import GroupCommand
31
31
  from airflow.configuration import conf
32
+ from airflow.executors import workloads
32
33
  from airflow.executors.base_executor import BaseExecutor
33
34
  from airflow.models.taskinstance import TaskInstance
34
- from airflow.providers.common.compat.sdk import timezone
35
+ from airflow.providers.common.compat.sdk import Stats, timezone
35
36
  from airflow.providers.edge3.cli.edge_command import EDGE_COMMANDS
36
37
  from airflow.providers.edge3.models.edge_job import EdgeJobModel
37
38
  from airflow.providers.edge3.models.edge_logs import EdgeLogsModel
38
39
  from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel, EdgeWorkerState, reset_metrics
39
- from airflow.providers.edge3.version_compat import AIRFLOW_V_3_0_PLUS
40
- from airflow.stats import Stats
41
40
  from airflow.utils.db import DBLocks, create_global_lock
42
41
  from airflow.utils.session import NEW_SESSION, provide_session
43
42
  from airflow.utils.state import TaskInstanceState
@@ -69,8 +68,10 @@ class EdgeExecutor(BaseExecutor):
69
68
  """
70
69
  Check if already existing table matches the newest table schema.
71
70
 
72
- workaround till support for Airflow 2.x is dropped,
71
+ workaround as Airflow 2.x had no support for provider DB migrations,
73
72
  then it is possible to use alembic also for provider distributions.
73
+
74
+ TODO(jscheffl): Change to alembic DB migrations in the future.
74
75
  """
75
76
  inspector = inspect(engine)
76
77
  edge_job_columns = None
@@ -125,66 +126,13 @@ class EdgeExecutor(BaseExecutor):
125
126
  self.edge_queued_tasks = deepcopy(self.queued_tasks)
126
127
  super()._process_tasks(task_tuples) # type: ignore[misc]
127
128
 
128
- @provide_session
129
- def execute_async(
130
- self,
131
- key: TaskInstanceKey,
132
- command: CommandType,
133
- queue: str | None = None,
134
- executor_config: Any | None = None,
135
- session: Session = NEW_SESSION,
136
- ) -> None:
137
- """Execute asynchronously. Airflow 2.10 entry point to execute a task."""
138
- # Use of a temporary trick to get task instance, will be changed with Airflow 3.0.0
139
- # code works together with _process_tasks overwrite to get task instance.
140
- # TaskInstance in fourth element
141
- task_instance = self.edge_queued_tasks[key][3] # type: ignore[index]
142
- del self.edge_queued_tasks[key]
143
-
144
- self.validate_airflow_tasks_run_command(command) # type: ignore[attr-defined]
145
-
146
- # Check if job already exists with same dag_id, task_id, run_id, map_index, try_number
147
- existing_job = (
148
- session.query(EdgeJobModel)
149
- .filter_by(
150
- dag_id=key.dag_id,
151
- task_id=key.task_id,
152
- run_id=key.run_id,
153
- map_index=key.map_index,
154
- try_number=key.try_number,
155
- )
156
- .first()
157
- )
158
-
159
- if existing_job:
160
- existing_job.state = TaskInstanceState.QUEUED
161
- existing_job.queue = queue or DEFAULT_QUEUE
162
- existing_job.concurrency_slots = task_instance.pool_slots
163
- existing_job.command = str(command)
164
- else:
165
- session.add(
166
- EdgeJobModel(
167
- dag_id=key.dag_id,
168
- task_id=key.task_id,
169
- run_id=key.run_id,
170
- map_index=key.map_index,
171
- try_number=key.try_number,
172
- state=TaskInstanceState.QUEUED,
173
- queue=queue or DEFAULT_QUEUE,
174
- concurrency_slots=task_instance.pool_slots,
175
- command=str(command),
176
- )
177
- )
178
-
179
129
  @provide_session
180
130
  def queue_workload(
181
131
  self,
182
- workload: Any, # Note actually "airflow.executors.workloads.All" but not existing in Airflow 2.10
132
+ workload: workloads.All,
183
133
  session: Session = NEW_SESSION,
184
134
  ) -> None:
185
135
  """Put new workload to queue. Airflow 3 entry point to execute a task."""
186
- from airflow.executors import workloads
187
-
188
136
  if not isinstance(workload, workloads.ExecuteTask):
189
137
  raise TypeError(f"Don't know how to queue workload of type {type(workload).__name__}")
190
138
 
@@ -263,11 +211,7 @@ class EdgeExecutor(BaseExecutor):
263
211
 
264
212
  def _update_orphaned_jobs(self, session: Session) -> bool:
265
213
  """Update status ob jobs when workers die and don't update anymore."""
266
- if AIRFLOW_V_3_0_PLUS:
267
- heartbeat_interval_config_name = "task_instance_heartbeat_timeout"
268
- else:
269
- heartbeat_interval_config_name = "scheduler_zombie_task_threshold"
270
- heartbeat_interval: int = conf.getint("scheduler", heartbeat_interval_config_name)
214
+ heartbeat_interval: int = conf.getint("scheduler", "task_instance_heartbeat_timeout")
271
215
  lifeless_jobs: list[EdgeJobModel] = (
272
216
  session.query(EdgeJobModel)
273
217
  .with_for_update(skip_locked=True)
@@ -93,6 +93,13 @@ def get_provider_info():
93
93
  "example": None,
94
94
  "default": "524288",
95
95
  },
96
+ "push_logs": {
97
+ "description": "Flag to enable or disable pushing of log files from edge worker to the central site.\nWhen enabled, edge workers will upload task log files in chunks to the central Airflow site.\nWhen disabled, logs will only be available locally on the edge worker.\n",
98
+ "version_added": "1.5.0",
99
+ "type": "boolean",
100
+ "example": "True",
101
+ "default": "True",
102
+ },
96
103
  "worker_umask": {
97
104
  "description": "The default umask to use for edge worker when run in daemon mode\n\nThis controls the file-creation mode mask which determines the initial value of file permission bits\nfor newly created files.\n\nThis value is treated as an octal-integer.\n",
98
105
  "version_added": None,
@@ -26,11 +26,9 @@ from typing import TYPE_CHECKING
26
26
  from sqlalchemy import Integer, String, delete, select
27
27
  from sqlalchemy.orm import Mapped
28
28
 
29
- from airflow.exceptions import AirflowException
30
29
  from airflow.models.base import Base
31
- from airflow.providers.common.compat.sdk import timezone
30
+ from airflow.providers.common.compat.sdk import AirflowException, Stats, timezone
32
31
  from airflow.providers.common.compat.sqlalchemy.orm import mapped_column
33
- from airflow.stats import Stats
34
32
  from airflow.utils.log.logging_mixin import LoggingMixin
35
33
  from airflow.utils.providers_configuration_loader import providers_configuration_loaded
36
34
  from airflow.utils.session import NEW_SESSION, provide_session
@@ -50,6 +48,12 @@ class EdgeWorkerVersionException(AirflowException):
50
48
  pass
51
49
 
52
50
 
51
+ class EdgeWorkerDuplicateException(AirflowException):
52
+ """Signal that a worker with the same name is already active."""
53
+
54
+ pass
55
+
56
+
53
57
  class EdgeWorkerState(str, Enum):
54
58
  """Status of a Edge Worker instance."""
55
59