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
@@ -23,188 +23,35 @@ from typing import TYPE_CHECKING, Any
23
23
  from airflow.configuration import conf
24
24
  from airflow.exceptions import AirflowConfigException
25
25
  from airflow.plugins_manager import AirflowPlugin
26
- from airflow.providers.edge3.version_compat import AIRFLOW_V_3_0_PLUS, AIRFLOW_V_3_1_PLUS
26
+ from airflow.providers.edge3.version_compat import AIRFLOW_V_3_1_PLUS
27
27
  from airflow.utils.session import NEW_SESSION, provide_session
28
28
 
29
29
  if TYPE_CHECKING:
30
30
  from sqlalchemy.orm import Session
31
31
 
32
- if AIRFLOW_V_3_0_PLUS:
33
- from airflow.utils.db import DBLocks, create_global_lock
32
+ from airflow.utils.db import DBLocks, create_global_lock
34
33
 
35
- @provide_session
36
- def _get_api_endpoint(session: Session = NEW_SESSION) -> dict[str, Any]:
37
- # Ensure all required DB modeals are created before starting the API
38
- with create_global_lock(session=session, lock=DBLocks.MIGRATIONS):
39
- engine = session.get_bind().engine
40
- from airflow.providers.edge3.models.edge_job import EdgeJobModel
41
- from airflow.providers.edge3.models.edge_logs import EdgeLogsModel
42
- from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel
43
34
 
44
- EdgeJobModel.metadata.create_all(engine)
45
- EdgeLogsModel.metadata.create_all(engine)
46
- EdgeWorkerModel.metadata.create_all(engine)
35
+ @provide_session
36
+ def _get_api_endpoint(session: Session = NEW_SESSION) -> dict[str, Any]:
37
+ # Ensure all required DB modeals are created before starting the API
38
+ with create_global_lock(session=session, lock=DBLocks.MIGRATIONS):
39
+ engine = session.get_bind().engine
40
+ from airflow.providers.edge3.models.edge_job import EdgeJobModel
41
+ from airflow.providers.edge3.models.edge_logs import EdgeLogsModel
42
+ from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel
47
43
 
48
- from airflow.providers.edge3.worker_api.app import create_edge_worker_api_app
44
+ EdgeJobModel.metadata.create_all(engine)
45
+ EdgeLogsModel.metadata.create_all(engine)
46
+ EdgeWorkerModel.metadata.create_all(engine)
49
47
 
50
- return {
51
- "app": create_edge_worker_api_app(),
52
- "url_prefix": "/edge_worker",
53
- "name": "Airflow Edge Worker",
54
- }
48
+ from airflow.providers.edge3.worker_api.app import create_edge_worker_api_app
55
49
 
56
- else:
57
- # This is for back-compatibility with Airflow 2.x and we only make this
58
- # to prevents dependencies and breaking imports in Airflow 3.x
59
- import re
60
- from datetime import datetime, timedelta
61
- from pathlib import Path
62
-
63
- from flask import Blueprint, redirect, request, url_for
64
- from flask_appbuilder import BaseView, expose
65
- from markupsafe import Markup
66
- from sqlalchemy import select
67
-
68
- from airflow.auth.managers.models.resource_details import AccessView
69
- from airflow.utils.state import State, TaskInstanceState
70
- from airflow.utils.yaml import safe_load
71
- from airflow.www.auth import has_access_view
72
-
73
- def _get_airflow_2_api_endpoint() -> Blueprint:
74
- from airflow.www.app import csrf
75
- from airflow.www.constants import SWAGGER_BUNDLE, SWAGGER_ENABLED
76
- from airflow.www.extensions.init_views import _CustomErrorRequestBodyValidator, _LazyResolver
77
-
78
- folder = Path(__file__).parents[1].resolve() # this is airflow/providers/edge3/
79
- with folder.joinpath("openapi", "edge_worker_api_v1.yaml").open() as f:
80
- specification = safe_load(f)
81
- from connexion import FlaskApi
82
-
83
- bp = FlaskApi(
84
- specification=specification,
85
- resolver=_LazyResolver(),
86
- base_path="/edge_worker/v1",
87
- strict_validation=True,
88
- options={"swagger_ui": SWAGGER_ENABLED, "swagger_path": SWAGGER_BUNDLE.__fspath__()},
89
- validate_responses=True,
90
- validator_map={"body": _CustomErrorRequestBodyValidator},
91
- ).blueprint
92
- # Need to exempt CSRF to make API usable
93
- csrf.exempt(bp)
94
- return bp
95
-
96
- def _state_token(state):
97
- """Return a formatted string with HTML for a given State."""
98
- color = State.color(state)
99
- fg_color = State.color_fg(state)
100
- return Markup(
101
- """
102
- <span class="label" style="color:{fg_color}; background-color:{color};"
103
- title="Current State: {state}">{state}</span>
104
- """
105
- ).format(color=color, state=state, fg_color=fg_color)
106
-
107
- def modify_maintenance_comment_on_update(maintenance_comment: str | None, username: str) -> str:
108
- if maintenance_comment:
109
- if re.search(
110
- r"^\[[-\d:\s]+\] - .+ put node into maintenance mode\r?\nComment:.*", maintenance_comment
111
- ):
112
- return re.sub(
113
- r"^\[[-\d:\s]+\] - .+ put node into maintenance mode\r?\nComment:",
114
- f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {username} updated maintenance mode\nComment:",
115
- maintenance_comment,
116
- )
117
- if re.search(r"^\[[-\d:\s]+\] - .+ updated maintenance mode\r?\nComment:.*", maintenance_comment):
118
- return re.sub(
119
- r"^\[[-\d:\s]+\] - .+ updated maintenance mode\r?\nComment:",
120
- f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {username} updated maintenance mode\nComment:",
121
- maintenance_comment,
122
- )
123
- return f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {username} updated maintenance mode\nComment: {maintenance_comment}"
124
- return (
125
- f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {username} updated maintenance mode\nComment:"
126
- )
127
-
128
- # registers airflow/providers/edge3/plugins/templates as a Jinja template folder
129
- template_bp = Blueprint(
130
- "template_blueprint",
131
- __name__,
132
- template_folder="templates",
133
- )
134
-
135
- class EdgeWorkerJobs(BaseView):
136
- """Simple view to show Edge Worker jobs."""
137
-
138
- default_view = "jobs"
139
-
140
- @expose("/jobs")
141
- @has_access_view(AccessView.JOBS)
142
- @provide_session
143
- def jobs(self, session: Session = NEW_SESSION):
144
- from airflow.providers.edge3.models.edge_job import EdgeJobModel
145
-
146
- jobs = session.scalars(select(EdgeJobModel).order_by(EdgeJobModel.queued_dttm)).all()
147
- html_states = {
148
- str(state): _state_token(str(state)) for state in TaskInstanceState.__members__.values()
149
- }
150
- return self.render_template("edge_worker_jobs.html", jobs=jobs, html_states=html_states)
151
-
152
- class EdgeWorkerHosts(BaseView):
153
- """Simple view to show Edge Worker status."""
154
-
155
- default_view = "status"
156
-
157
- @expose("/status")
158
- @has_access_view(AccessView.JOBS)
159
- @provide_session
160
- def status(self, session: Session = NEW_SESSION):
161
- from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel
162
-
163
- hosts = session.scalars(select(EdgeWorkerModel).order_by(EdgeWorkerModel.worker_name)).all()
164
- five_min_ago = datetime.now() - timedelta(minutes=5)
165
- return self.render_template("edge_worker_hosts.html", hosts=hosts, five_min_ago=five_min_ago)
166
-
167
- @expose("/status/maintenance/<string:worker_name>/on", methods=["POST"])
168
- @has_access_view(AccessView.JOBS)
169
- def worker_to_maintenance(self, worker_name: str):
170
- from flask_login import current_user
171
-
172
- from airflow.providers.edge3.models.edge_worker import request_maintenance
173
-
174
- maintenance_comment = request.form.get("maintenance_comment")
175
- maintenance_comment = f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {current_user.username} put node into maintenance mode\nComment: {maintenance_comment}"
176
- request_maintenance(worker_name, maintenance_comment)
177
- return redirect(url_for("EdgeWorkerHosts.status"))
178
-
179
- @expose("/status/maintenance/<string:worker_name>/off", methods=["POST"])
180
- @has_access_view(AccessView.JOBS)
181
- def remove_worker_from_maintenance(self, worker_name: str):
182
- from airflow.providers.edge3.models.edge_worker import exit_maintenance
183
-
184
- exit_maintenance(worker_name)
185
- return redirect(url_for("EdgeWorkerHosts.status"))
186
-
187
- @expose("/status/maintenance/<string:worker_name>/remove", methods=["POST"])
188
- @has_access_view(AccessView.JOBS)
189
- def remove_worker(self, worker_name: str):
190
- from airflow.providers.edge3.models.edge_worker import remove_worker
191
-
192
- remove_worker(worker_name)
193
- return redirect(url_for("EdgeWorkerHosts.status"))
194
-
195
- @expose("/status/maintenance/<string:worker_name>/change_comment", methods=["POST"])
196
- @has_access_view(AccessView.JOBS)
197
- def change_maintenance_comment(self, worker_name: str):
198
- from flask_login import current_user
199
-
200
- from airflow.providers.edge3.models.edge_worker import change_maintenance_comment
201
-
202
- maintenance_comment = request.form.get("maintenance_comment")
203
- maintenance_comment = modify_maintenance_comment_on_update(
204
- maintenance_comment, current_user.username
205
- )
206
- change_maintenance_comment(worker_name, maintenance_comment)
207
- return redirect(url_for("EdgeWorkerHosts.status"))
50
+ return {
51
+ "app": create_edge_worker_api_app(),
52
+ "url_prefix": "/edge_worker",
53
+ "name": "Airflow Edge Worker",
54
+ }
208
55
 
209
56
 
210
57
  # Check if EdgeExecutor is actually loaded
@@ -213,17 +60,14 @@ try:
213
60
  except AirflowConfigException:
214
61
  EDGE_EXECUTOR_ACTIVE = False
215
62
 
216
- # Load the API endpoint only on api-server (Airflow 3.x) or webserver (Airflow 2.x)
217
- # todo(jscheffl): Remove this check when the discussion in
63
+ # Load the API endpoint only on api-server
64
+ # TODO(jscheffl): Remove this check when the discussion in
218
65
  # https://lists.apache.org/thread/w170czq6r7bslkqp1tk6bjjjo0789wgl
219
66
  # resulted in a proper API to selective initialize. Maybe backcompat-shim
220
67
  # is also needed to support Airflow-versions prior the rework.
221
- if AIRFLOW_V_3_0_PLUS:
222
- RUNNING_ON_APISERVER = (len(sys.argv) > 1 and sys.argv[1] in ["api-server"]) or (
223
- len(sys.argv) > 2 and sys.argv[2] == "airflow-core/src/airflow/api_fastapi/main.py"
224
- )
225
- else:
226
- RUNNING_ON_APISERVER = "gunicorn" in sys.argv[0] and "airflow-webserver" in sys.argv
68
+ RUNNING_ON_APISERVER = (len(sys.argv) > 1 and sys.argv[1] in ["api-server"]) or (
69
+ len(sys.argv) > 2 and sys.argv[2] == "airflow-core/src/airflow/api_fastapi/main.py"
70
+ )
227
71
 
228
72
 
229
73
  def _get_base_url_path(path: str) -> str:
@@ -247,8 +91,9 @@ class EdgeExecutorPlugin(AirflowPlugin):
247
91
 
248
92
  name = "edge_executor"
249
93
  if EDGE_EXECUTOR_ACTIVE and RUNNING_ON_APISERVER:
94
+ fastapi_apps = [_get_api_endpoint()]
250
95
  if AIRFLOW_V_3_1_PLUS:
251
- fastapi_apps = [_get_api_endpoint()]
96
+ # Airflow 3.0 does not know about react_apps, so we only provide the API endpoint
252
97
  react_apps = [
253
98
  {
254
99
  "name": "Edge Executor",
@@ -271,27 +116,3 @@ class EdgeExecutorPlugin(AirflowPlugin):
271
116
  "url_route": "edge_worker_api_docs",
272
117
  }
273
118
  ]
274
- if AIRFLOW_V_3_0_PLUS:
275
- # Airflow 3.0 does not know about react_apps, so we only provide the API endpoint
276
- fastapi_apps = [_get_api_endpoint()]
277
- else:
278
- appbuilder_menu_items = [
279
- {
280
- "name": "Edge Worker API docs",
281
- "href": _get_base_url_path("/edge_worker/v1/ui"),
282
- "category": "Docs",
283
- }
284
- ]
285
- appbuilder_views = [
286
- {
287
- "name": "Edge Worker Jobs",
288
- "category": "Admin",
289
- "view": EdgeWorkerJobs(),
290
- },
291
- {
292
- "name": "Edge Worker Hosts",
293
- "category": "Admin",
294
- "view": EdgeWorkerHosts(),
295
- },
296
- ]
297
- flask_blueprints = [_get_airflow_2_api_endpoint(), template_bp]