apache-airflow-providers-edge3 1.3.1__py3-none-any.whl → 1.4.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.
- airflow/providers/edge3/__init__.py +1 -1
- airflow/providers/edge3/cli/worker.py +1 -1
- airflow/providers/edge3/example_dags/win_test.py +1 -1
- airflow/providers/edge3/executors/edge_executor.py +31 -2
- airflow/providers/edge3/models/edge_job.py +17 -14
- airflow/providers/edge3/models/edge_logs.py +11 -8
- airflow/providers/edge3/models/edge_worker.py +23 -19
- airflow/providers/edge3/openapi/v2-edge-generated.yaml +3 -3
- airflow/providers/edge3/plugins/edge_executor_plugin.py +26 -10
- airflow/providers/edge3/plugins/www/dist/main.umd.cjs +19 -19
- airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts +4 -4
- airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts +1 -1
- airflow/providers/edge3/plugins/www/src/layouts/EdgeLayout.tsx +5 -1
- airflow/providers/edge3/plugins/www/src/main.tsx +6 -0
- airflow/providers/edge3/version_compat.py +5 -0
- airflow/providers/edge3/worker_api/routes/jobs.py +1 -1
- airflow/providers/edge3/worker_api/routes/worker.py +1 -1
- {apache_airflow_providers_edge3-1.3.1.dist-info → apache_airflow_providers_edge3-1.4.0.dist-info}/METADATA +34 -13
- {apache_airflow_providers_edge3-1.3.1.dist-info → apache_airflow_providers_edge3-1.4.0.dist-info}/RECORD +21 -21
- {apache_airflow_providers_edge3-1.3.1.dist-info → apache_airflow_providers_edge3-1.4.0.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_edge3-1.3.1.dist-info → apache_airflow_providers_edge3-1.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
|
|
|
29
29
|
|
|
30
30
|
__all__ = ["__version__"]
|
|
31
31
|
|
|
32
|
-
__version__ = "1.
|
|
32
|
+
__version__ = "1.4.0"
|
|
33
33
|
|
|
34
34
|
if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
|
|
35
35
|
"2.10.0"
|
|
@@ -34,6 +34,7 @@ from requests import HTTPError
|
|
|
34
34
|
|
|
35
35
|
from airflow import __version__ as airflow_version
|
|
36
36
|
from airflow.configuration import conf
|
|
37
|
+
from airflow.providers.common.compat.sdk import timezone
|
|
37
38
|
from airflow.providers.edge3 import __version__ as edge_provider_version
|
|
38
39
|
from airflow.providers.edge3.cli.api_client import (
|
|
39
40
|
jobs_fetch,
|
|
@@ -52,7 +53,6 @@ from airflow.providers.edge3.cli.signalling import (
|
|
|
52
53
|
)
|
|
53
54
|
from airflow.providers.edge3.models.edge_worker import EdgeWorkerState, EdgeWorkerVersionException
|
|
54
55
|
from airflow.providers.edge3.version_compat import AIRFLOW_V_3_0_PLUS
|
|
55
|
-
from airflow.utils import timezone
|
|
56
56
|
from airflow.utils.net import getfqdn
|
|
57
57
|
from airflow.utils.state import TaskInstanceState
|
|
58
58
|
|
|
@@ -67,7 +67,7 @@ if TYPE_CHECKING:
|
|
|
67
67
|
try:
|
|
68
68
|
from airflow.operators.python import PythonOperator
|
|
69
69
|
except ImportError:
|
|
70
|
-
from airflow.providers.common.compat.standard.operators import PythonOperator
|
|
70
|
+
from airflow.providers.common.compat.standard.operators import PythonOperator # type: ignore[no-redef]
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
class CmdOperator(BaseOperator):
|
|
@@ -31,13 +31,13 @@ from airflow.cli.cli_config import GroupCommand
|
|
|
31
31
|
from airflow.configuration import conf
|
|
32
32
|
from airflow.executors.base_executor import BaseExecutor
|
|
33
33
|
from airflow.models.taskinstance import TaskInstance, TaskInstanceState
|
|
34
|
+
from airflow.providers.common.compat.sdk import timezone
|
|
34
35
|
from airflow.providers.edge3.cli.edge_command import EDGE_COMMANDS
|
|
35
36
|
from airflow.providers.edge3.models.edge_job import EdgeJobModel
|
|
36
37
|
from airflow.providers.edge3.models.edge_logs import EdgeLogsModel
|
|
37
38
|
from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel, EdgeWorkerState, reset_metrics
|
|
38
39
|
from airflow.providers.edge3.version_compat import AIRFLOW_V_3_0_PLUS
|
|
39
40
|
from airflow.stats import Stats
|
|
40
|
-
from airflow.utils import timezone
|
|
41
41
|
from airflow.utils.db import DBLocks, create_global_lock
|
|
42
42
|
from airflow.utils.session import NEW_SESSION, provide_session
|
|
43
43
|
|
|
@@ -352,7 +352,7 @@ class EdgeExecutor(BaseExecutor):
|
|
|
352
352
|
del self.last_reported_state[job.key]
|
|
353
353
|
self.fail(job.key)
|
|
354
354
|
else:
|
|
355
|
-
self.last_reported_state[job.key] = job.state
|
|
355
|
+
self.last_reported_state[job.key] = TaskInstanceState(job.state)
|
|
356
356
|
if (
|
|
357
357
|
job.state == TaskInstanceState.SUCCESS
|
|
358
358
|
and job.last_update_t < (datetime.now() - timedelta(minutes=job_success_purge)).timestamp()
|
|
@@ -399,6 +399,35 @@ class EdgeExecutor(BaseExecutor):
|
|
|
399
399
|
def terminate(self):
|
|
400
400
|
"""Terminate the executor is not doing anything."""
|
|
401
401
|
|
|
402
|
+
@provide_session
|
|
403
|
+
def revoke_task(self, *, ti: TaskInstance, session: Session = NEW_SESSION):
|
|
404
|
+
"""
|
|
405
|
+
Revoke a task instance from the executor.
|
|
406
|
+
|
|
407
|
+
This method removes the task from the executor's internal state and deletes
|
|
408
|
+
the corresponding EdgeJobModel record to prevent edge workers from picking it up.
|
|
409
|
+
|
|
410
|
+
:param ti: Task instance to revoke
|
|
411
|
+
:param session: Database session
|
|
412
|
+
"""
|
|
413
|
+
# Remove from executor's internal state
|
|
414
|
+
self.running.discard(ti.key)
|
|
415
|
+
self.queued_tasks.pop(ti.key, None)
|
|
416
|
+
if ti.key in self.last_reported_state:
|
|
417
|
+
del self.last_reported_state[ti.key]
|
|
418
|
+
|
|
419
|
+
# Delete the job from the database to prevent edge workers from picking it up
|
|
420
|
+
session.execute(
|
|
421
|
+
delete(EdgeJobModel).where(
|
|
422
|
+
EdgeJobModel.dag_id == ti.dag_id,
|
|
423
|
+
EdgeJobModel.task_id == ti.task_id,
|
|
424
|
+
EdgeJobModel.run_id == ti.run_id,
|
|
425
|
+
EdgeJobModel.map_index == ti.map_index,
|
|
426
|
+
EdgeJobModel.try_number == ti.try_number,
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
self.log.info("Revoked task instance %s from EdgeExecutor", ti.key)
|
|
430
|
+
|
|
402
431
|
def try_adopt_task_instances(self, tis: Sequence[TaskInstance]) -> Sequence[TaskInstance]:
|
|
403
432
|
"""
|
|
404
433
|
Try to adopt running task instances that have been abandoned by a SchedulerJob dying.
|
|
@@ -19,16 +19,17 @@ from __future__ import annotations
|
|
|
19
19
|
from datetime import datetime
|
|
20
20
|
|
|
21
21
|
from sqlalchemy import (
|
|
22
|
-
Column,
|
|
23
22
|
Index,
|
|
24
23
|
Integer,
|
|
25
24
|
String,
|
|
26
25
|
text,
|
|
27
26
|
)
|
|
27
|
+
from sqlalchemy.orm import Mapped
|
|
28
28
|
|
|
29
29
|
from airflow.models.base import Base, StringID
|
|
30
30
|
from airflow.models.taskinstancekey import TaskInstanceKey
|
|
31
|
-
from airflow.
|
|
31
|
+
from airflow.providers.common.compat.sdk import timezone
|
|
32
|
+
from airflow.providers.common.compat.sqlalchemy.orm import mapped_column
|
|
32
33
|
from airflow.utils.log.logging_mixin import LoggingMixin
|
|
33
34
|
from airflow.utils.sqlalchemy import UtcDateTime
|
|
34
35
|
|
|
@@ -41,18 +42,20 @@ class EdgeJobModel(Base, LoggingMixin):
|
|
|
41
42
|
"""
|
|
42
43
|
|
|
43
44
|
__tablename__ = "edge_job"
|
|
44
|
-
dag_id =
|
|
45
|
-
task_id =
|
|
46
|
-
run_id =
|
|
47
|
-
map_index
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
dag_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
|
|
46
|
+
task_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
|
|
47
|
+
run_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
|
|
48
|
+
map_index: Mapped[int] = mapped_column(
|
|
49
|
+
Integer, primary_key=True, nullable=False, server_default=text("-1")
|
|
50
|
+
)
|
|
51
|
+
try_number: Mapped[int] = mapped_column(Integer, primary_key=True, default=0)
|
|
52
|
+
state: Mapped[str] = mapped_column(String(20))
|
|
53
|
+
queue: Mapped[str] = mapped_column(String(256))
|
|
54
|
+
concurrency_slots: Mapped[int] = mapped_column(Integer)
|
|
55
|
+
command: Mapped[str] = mapped_column(String(2048))
|
|
56
|
+
queued_dttm: Mapped[datetime | None] = mapped_column(UtcDateTime)
|
|
57
|
+
edge_worker: Mapped[str | None] = mapped_column(String(64))
|
|
58
|
+
last_update: Mapped[datetime | None] = mapped_column(UtcDateTime)
|
|
56
59
|
|
|
57
60
|
def __init__(
|
|
58
61
|
self,
|
|
@@ -19,14 +19,15 @@ from __future__ import annotations
|
|
|
19
19
|
from datetime import datetime
|
|
20
20
|
|
|
21
21
|
from sqlalchemy import (
|
|
22
|
-
Column,
|
|
23
22
|
Integer,
|
|
24
23
|
Text,
|
|
25
24
|
text,
|
|
26
25
|
)
|
|
27
26
|
from sqlalchemy.dialects.mysql import MEDIUMTEXT
|
|
27
|
+
from sqlalchemy.orm import Mapped
|
|
28
28
|
|
|
29
29
|
from airflow.models.base import Base, StringID
|
|
30
|
+
from airflow.providers.common.compat.sqlalchemy.orm import mapped_column
|
|
30
31
|
from airflow.utils.log.logging_mixin import LoggingMixin
|
|
31
32
|
from airflow.utils.sqlalchemy import UtcDateTime
|
|
32
33
|
|
|
@@ -45,13 +46,15 @@ class EdgeLogsModel(Base, LoggingMixin):
|
|
|
45
46
|
"""
|
|
46
47
|
|
|
47
48
|
__tablename__ = "edge_logs"
|
|
48
|
-
dag_id =
|
|
49
|
-
task_id =
|
|
50
|
-
run_id =
|
|
51
|
-
map_index
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
dag_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
|
|
50
|
+
task_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
|
|
51
|
+
run_id: Mapped[str] = mapped_column(StringID(), primary_key=True, nullable=False)
|
|
52
|
+
map_index: Mapped[int] = mapped_column(
|
|
53
|
+
Integer, primary_key=True, nullable=False, server_default=text("-1")
|
|
54
|
+
)
|
|
55
|
+
try_number: Mapped[int] = mapped_column(Integer, primary_key=True, default=0)
|
|
56
|
+
log_chunk_time: Mapped[datetime] = mapped_column(UtcDateTime, primary_key=True, nullable=False)
|
|
57
|
+
log_chunk_data: Mapped[str] = mapped_column(Text().with_variant(MEDIUMTEXT(), "mysql"), nullable=False)
|
|
55
58
|
|
|
56
59
|
def __init__(
|
|
57
60
|
self,
|
|
@@ -23,18 +23,22 @@ from datetime import datetime
|
|
|
23
23
|
from enum import Enum
|
|
24
24
|
from typing import TYPE_CHECKING
|
|
25
25
|
|
|
26
|
-
from sqlalchemy import
|
|
26
|
+
from sqlalchemy import Integer, String, delete, select
|
|
27
|
+
from sqlalchemy.orm import Mapped
|
|
27
28
|
|
|
28
29
|
from airflow.exceptions import AirflowException
|
|
29
30
|
from airflow.models.base import Base
|
|
31
|
+
from airflow.providers.common.compat.sdk import timezone
|
|
32
|
+
from airflow.providers.common.compat.sqlalchemy.orm import mapped_column
|
|
30
33
|
from airflow.stats import Stats
|
|
31
|
-
from airflow.utils import timezone
|
|
32
34
|
from airflow.utils.log.logging_mixin import LoggingMixin
|
|
33
35
|
from airflow.utils.providers_configuration_loader import providers_configuration_loaded
|
|
34
36
|
from airflow.utils.session import NEW_SESSION, provide_session
|
|
35
37
|
from airflow.utils.sqlalchemy import UtcDateTime
|
|
36
38
|
|
|
37
39
|
if TYPE_CHECKING:
|
|
40
|
+
from collections.abc import Sequence
|
|
41
|
+
|
|
38
42
|
from sqlalchemy.orm import Session
|
|
39
43
|
|
|
40
44
|
logger = logging.getLogger(__name__)
|
|
@@ -79,29 +83,29 @@ class EdgeWorkerModel(Base, LoggingMixin):
|
|
|
79
83
|
"""A Edge Worker instance which reports the state and health."""
|
|
80
84
|
|
|
81
85
|
__tablename__ = "edge_worker"
|
|
82
|
-
worker_name =
|
|
83
|
-
state =
|
|
84
|
-
maintenance_comment =
|
|
85
|
-
_queues =
|
|
86
|
-
first_online =
|
|
87
|
-
last_update =
|
|
88
|
-
jobs_active =
|
|
89
|
-
jobs_taken =
|
|
90
|
-
jobs_success =
|
|
91
|
-
jobs_failed =
|
|
92
|
-
sysinfo =
|
|
86
|
+
worker_name: Mapped[str] = mapped_column(String(64), primary_key=True, nullable=False)
|
|
87
|
+
state: Mapped[EdgeWorkerState] = mapped_column(String(20))
|
|
88
|
+
maintenance_comment: Mapped[str | None] = mapped_column(String(1024))
|
|
89
|
+
_queues: Mapped[str | None] = mapped_column("queues", String(256))
|
|
90
|
+
first_online: Mapped[datetime | None] = mapped_column(UtcDateTime)
|
|
91
|
+
last_update: Mapped[datetime | None] = mapped_column(UtcDateTime)
|
|
92
|
+
jobs_active: Mapped[int] = mapped_column(Integer, default=0)
|
|
93
|
+
jobs_taken: Mapped[int] = mapped_column(Integer, default=0)
|
|
94
|
+
jobs_success: Mapped[int] = mapped_column(Integer, default=0)
|
|
95
|
+
jobs_failed: Mapped[int] = mapped_column(Integer, default=0)
|
|
96
|
+
sysinfo: Mapped[str | None] = mapped_column(String(256))
|
|
93
97
|
|
|
94
98
|
def __init__(
|
|
95
99
|
self,
|
|
96
100
|
worker_name: str,
|
|
97
|
-
state: str,
|
|
101
|
+
state: str | EdgeWorkerState,
|
|
98
102
|
queues: list[str] | None,
|
|
99
103
|
first_online: datetime | None = None,
|
|
100
104
|
last_update: datetime | None = None,
|
|
101
105
|
maintenance_comment: str | None = None,
|
|
102
106
|
):
|
|
103
107
|
self.worker_name = worker_name
|
|
104
|
-
self.state = state
|
|
108
|
+
self.state = EdgeWorkerState(state)
|
|
105
109
|
self.queues = queues
|
|
106
110
|
self.first_online = first_online or timezone.utcnow()
|
|
107
111
|
self.last_update = last_update
|
|
@@ -139,14 +143,14 @@ class EdgeWorkerModel(Base, LoggingMixin):
|
|
|
139
143
|
queues.remove(queue_name)
|
|
140
144
|
self.queues = queues
|
|
141
145
|
|
|
142
|
-
def update_state(self, state: str) -> None:
|
|
146
|
+
def update_state(self, state: str | EdgeWorkerState) -> None:
|
|
143
147
|
"""Update state field."""
|
|
144
|
-
self.state = state
|
|
148
|
+
self.state = EdgeWorkerState(state)
|
|
145
149
|
|
|
146
150
|
|
|
147
151
|
def set_metrics(
|
|
148
152
|
worker_name: str,
|
|
149
|
-
state: EdgeWorkerState,
|
|
153
|
+
state: str | EdgeWorkerState,
|
|
150
154
|
jobs_active: int,
|
|
151
155
|
concurrency: int,
|
|
152
156
|
free_concurrency: int,
|
|
@@ -204,7 +208,7 @@ def reset_metrics(worker_name: str) -> None:
|
|
|
204
208
|
@provide_session
|
|
205
209
|
def _fetch_edge_hosts_from_db(
|
|
206
210
|
hostname: str | None = None, states: list | None = None, session: Session = NEW_SESSION
|
|
207
|
-
) ->
|
|
211
|
+
) -> Sequence[EdgeWorkerModel]:
|
|
208
212
|
query = select(EdgeWorkerModel)
|
|
209
213
|
if states:
|
|
210
214
|
query = query.where(EdgeWorkerModel.state.in_(states))
|
|
@@ -886,8 +886,6 @@ components:
|
|
|
886
886
|
token:
|
|
887
887
|
type: string
|
|
888
888
|
title: Token
|
|
889
|
-
ti:
|
|
890
|
-
$ref: '#/components/schemas/TaskInstance'
|
|
891
889
|
dag_rel_path:
|
|
892
890
|
type: string
|
|
893
891
|
format: path
|
|
@@ -899,6 +897,8 @@ components:
|
|
|
899
897
|
- type: string
|
|
900
898
|
- type: 'null'
|
|
901
899
|
title: Log Path
|
|
900
|
+
ti:
|
|
901
|
+
$ref: '#/components/schemas/TaskInstance'
|
|
902
902
|
type:
|
|
903
903
|
type: string
|
|
904
904
|
const: ExecuteTask
|
|
@@ -907,10 +907,10 @@ components:
|
|
|
907
907
|
type: object
|
|
908
908
|
required:
|
|
909
909
|
- token
|
|
910
|
-
- ti
|
|
911
910
|
- dag_rel_path
|
|
912
911
|
- bundle_info
|
|
913
912
|
- log_path
|
|
913
|
+
- ti
|
|
914
914
|
title: ExecuteTask
|
|
915
915
|
description: Execute the given Task.
|
|
916
916
|
HTTPExceptionResponse:
|
|
@@ -227,6 +227,22 @@ else:
|
|
|
227
227
|
RUNNING_ON_APISERVER = "gunicorn" in sys.argv[0] and "airflow-webserver" in sys.argv
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
def _get_base_url_path(path: str) -> str:
|
|
231
|
+
"""Construct URL path with webserver base_url prefix."""
|
|
232
|
+
base_url = conf.get("api", "base_url", fallback="/")
|
|
233
|
+
# Extract pathname from base_url (handles both full URLs and path-only)
|
|
234
|
+
if base_url.startswith(("http://", "https://")):
|
|
235
|
+
from urllib.parse import urlparse
|
|
236
|
+
|
|
237
|
+
base_path = urlparse(base_url).path
|
|
238
|
+
else:
|
|
239
|
+
base_path = base_url
|
|
240
|
+
|
|
241
|
+
# Normalize paths: remove trailing slash from base, ensure leading slash on path
|
|
242
|
+
base_path = base_path.rstrip("/")
|
|
243
|
+
return base_path + path
|
|
244
|
+
|
|
245
|
+
|
|
230
246
|
class EdgeExecutorPlugin(AirflowPlugin):
|
|
231
247
|
"""EdgeExecutor Plugin - provides API endpoints for Edge Workers in Webserver."""
|
|
232
248
|
|
|
@@ -237,30 +253,30 @@ class EdgeExecutorPlugin(AirflowPlugin):
|
|
|
237
253
|
react_apps = [
|
|
238
254
|
{
|
|
239
255
|
"name": "Edge Worker",
|
|
240
|
-
"bundle_url": "/edge_worker/static/main.umd.cjs",
|
|
256
|
+
"bundle_url": _get_base_url_path("/edge_worker/static/main.umd.cjs"),
|
|
241
257
|
"destination": "nav",
|
|
242
258
|
"url_route": "edge_worker",
|
|
243
259
|
"category": "admin",
|
|
244
|
-
"icon": "/edge_worker/res/cloud-computer.svg",
|
|
245
|
-
"icon_dark_mode": "/edge_worker/res/cloud-computer-dark.svg",
|
|
260
|
+
"icon": _get_base_url_path("/edge_worker/res/cloud-computer.svg"),
|
|
261
|
+
"icon_dark_mode": _get_base_url_path("/edge_worker/res/cloud-computer-dark.svg"),
|
|
246
262
|
},
|
|
247
263
|
{
|
|
248
264
|
"name": "Edge Worker Jobs",
|
|
249
|
-
"bundle_url": "/edge_worker/static/main.umd.cjs",
|
|
265
|
+
"bundle_url": _get_base_url_path("/edge_worker/static/main.umd.cjs"),
|
|
250
266
|
"url_route": "edge_jobs",
|
|
251
267
|
"category": "admin",
|
|
252
|
-
"icon": "/edge_worker/res/cloud-computer.svg",
|
|
253
|
-
"icon_dark_mode": "/edge_worker/res/cloud-computer-dark.svg",
|
|
268
|
+
"icon": _get_base_url_path("/edge_worker/res/cloud-computer.svg"),
|
|
269
|
+
"icon_dark_mode": _get_base_url_path("/edge_worker/res/cloud-computer-dark.svg"),
|
|
254
270
|
},
|
|
255
271
|
]
|
|
256
272
|
external_views = [
|
|
257
273
|
{
|
|
258
274
|
"name": "Edge Worker API docs",
|
|
259
|
-
"href": "/edge_worker/docs",
|
|
275
|
+
"href": _get_base_url_path("/edge_worker/docs"),
|
|
260
276
|
"destination": "nav",
|
|
261
277
|
"category": "docs",
|
|
262
|
-
"icon": "/edge_worker/res/cloud-computer.svg",
|
|
263
|
-
"icon_dark_mode": "/edge_worker/res/cloud-computer-dark.svg",
|
|
278
|
+
"icon": _get_base_url_path("/edge_worker/res/cloud-computer.svg"),
|
|
279
|
+
"icon_dark_mode": _get_base_url_path("/edge_worker/res/cloud-computer-dark.svg"),
|
|
264
280
|
"url_route": "edge_worker_api_docs",
|
|
265
281
|
}
|
|
266
282
|
]
|
|
@@ -271,7 +287,7 @@ class EdgeExecutorPlugin(AirflowPlugin):
|
|
|
271
287
|
appbuilder_menu_items = [
|
|
272
288
|
{
|
|
273
289
|
"name": "Edge Worker API docs",
|
|
274
|
-
"href": "/edge_worker/v1/ui",
|
|
290
|
+
"href": _get_base_url_path("/edge_worker/v1/ui"),
|
|
275
291
|
"category": "Docs",
|
|
276
292
|
}
|
|
277
293
|
]
|