airflow-priority 0.1.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.
@@ -0,0 +1,3 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .common import *
@@ -0,0 +1,49 @@
1
+ from types import MappingProxyType
2
+ from typing import Literal, Optional, Tuple
3
+
4
+ from airflow.models.dagrun import DagRun
5
+
6
+ __all__ = (
7
+ "has_priority_tag",
8
+ "get_config_option",
9
+ "DagStatus",
10
+ "PriorityTags",
11
+ "AirflowPriorityConfigurationOptionNotFound",
12
+ )
13
+
14
+
15
+ DagStatus = Literal["running", "success", "failed"]
16
+
17
+ PriorityTags = MappingProxyType(
18
+ {
19
+ "P1": 1,
20
+ "P2": 2,
21
+ "P3": 3,
22
+ "P4": 4,
23
+ "P5": 5,
24
+ }
25
+ )
26
+
27
+
28
+ class AirflowPriorityConfigurationOptionNotFound(RuntimeError): ...
29
+
30
+
31
+ def get_config_option(section, key, default, required=True):
32
+ import airflow.configuration
33
+
34
+ config_option = airflow.configuration.conf.get(f"priority.{section}", key, default)
35
+ if not config_option and required:
36
+ raise AirflowPriorityConfigurationOptionNotFound(f"{section}.{key}")
37
+ return config_option
38
+
39
+
40
+ def has_priority_tag(dag_run: DagRun) -> Optional[Tuple[str, int]]:
41
+ dag = dag_run.dag
42
+ dag_id = dag_run.dag_id
43
+ tags = dag.tags
44
+ if tags:
45
+ for tag in tags:
46
+ priority = PriorityTags.get(tag.upper())
47
+ if priority:
48
+ return dag_id, priority
49
+ return dag_id, None
File without changes
@@ -0,0 +1,65 @@
1
+ import os
2
+ import sys
3
+ from datetime import datetime
4
+
5
+ from airflow.listeners import hookimpl
6
+ from airflow.models.dagrun import DagRun
7
+ from airflow.plugins_manager import AirflowPlugin
8
+ from boto3 import client as Boto3Client
9
+ from botocore.config import Config
10
+
11
+ from airflow_priority import DagStatus, has_priority_tag
12
+
13
+ config = Config(retries=dict(max_attempts=10, mode="adaptive"))
14
+ client = Boto3Client("cloudwatch", config=config)
15
+
16
+
17
+ def send_metric_cloudwatch(dag_id: str, priority: int, tag: DagStatus) -> None:
18
+ client.put_metric_data(
19
+ Namespace="Airflow/Custom",
20
+ MetricData=[
21
+ {
22
+ "MetricName": f"priority_{tag}",
23
+ "Dimensions": [
24
+ {"Name": "environment", "Value": os.environ.get("AIRFLOW_ENV_NAME", "unknown-airflow-env")},
25
+ {
26
+ "Name": "dag",
27
+ "Value": dag_id,
28
+ },
29
+ {
30
+ "Name": "priority",
31
+ "Value": str(priority),
32
+ },
33
+ ],
34
+ "Timestamp": datetime.utcnow(),
35
+ "Value": int(priority),
36
+ "Unit": "Count",
37
+ }
38
+ ],
39
+ )
40
+
41
+
42
+ @hookimpl
43
+ def on_dag_run_running(dag_run: DagRun, msg: str):
44
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
45
+ if priority:
46
+ send_metric_cloudwatch(dag_id, priority, "running")
47
+
48
+
49
+ @hookimpl
50
+ def on_dag_run_success(dag_run: DagRun, msg: str):
51
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
52
+ if priority:
53
+ send_metric_cloudwatch(dag_id, priority, "success")
54
+
55
+
56
+ @hookimpl
57
+ def on_dag_run_failed(dag_run: DagRun, msg: str):
58
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
59
+ if priority:
60
+ send_metric_cloudwatch(dag_id, priority, "failed")
61
+
62
+
63
+ class AWSCloudWatchPriorityPlugin(AirflowPlugin):
64
+ name = "AWSCloudWatchPriorityPlugin"
65
+ listeners = [sys.modules[__name__]]
@@ -0,0 +1,79 @@
1
+ import sys
2
+ from datetime import datetime
3
+ from functools import lru_cache
4
+
5
+ from airflow.listeners import hookimpl
6
+ from airflow.models.dagrun import DagRun
7
+ from airflow.plugins_manager import AirflowPlugin
8
+ from datadog_api_client import ApiClient, Configuration
9
+ from datadog_api_client.v2.api.metrics_api import MetricsApi
10
+ from datadog_api_client.v2.model.metric_intake_type import MetricIntakeType
11
+ from datadog_api_client.v2.model.metric_payload import MetricPayload
12
+ from datadog_api_client.v2.model.metric_point import MetricPoint
13
+ from datadog_api_client.v2.model.metric_resource import MetricResource
14
+ from datadog_api_client.v2.model.metric_series import MetricSeries
15
+
16
+ from airflow_priority import DagStatus, get_config_option, has_priority_tag
17
+
18
+
19
+ @lru_cache
20
+ def get_configuration():
21
+ return Configuration(
22
+ api_key={"apiKeyAuth": get_config_option("datadog", "api_key", "")},
23
+ )
24
+
25
+
26
+ def send_metric_datadog(dag_id: str, priority: int, tag: DagStatus) -> None:
27
+ with ApiClient(get_configuration()) as api_client:
28
+ api_instance = MetricsApi(api_client)
29
+
30
+ body = MetricPayload(
31
+ series=[
32
+ MetricSeries(
33
+ metric=f"airflow.custom.priority.p{priority}.{tag}",
34
+ type=MetricIntakeType.UNSPECIFIED,
35
+ points=[
36
+ MetricPoint(
37
+ timestamp=int(datetime.now().timestamp()),
38
+ value=priority,
39
+ ),
40
+ ],
41
+ resources=[
42
+ MetricResource(
43
+ name=dag_id,
44
+ type="dag",
45
+ ),
46
+ ],
47
+ tags=["airflow", "priority", f"dag_{dag_id}"],
48
+ ),
49
+ ],
50
+ )
51
+
52
+ resp = api_instance.submit_metrics(body=body)
53
+ assert resp["errors"] == []
54
+
55
+
56
+ @hookimpl
57
+ def on_dag_run_running(dag_run: DagRun, msg: str):
58
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
59
+ if priority:
60
+ send_metric_datadog(dag_id, priority, "running")
61
+
62
+
63
+ @hookimpl
64
+ def on_dag_run_success(dag_run: DagRun, msg: str):
65
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
66
+ if priority:
67
+ send_metric_datadog(dag_id, priority, "success")
68
+
69
+
70
+ @hookimpl
71
+ def on_dag_run_failed(dag_run: DagRun, msg: str):
72
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
73
+ if priority:
74
+ send_metric_datadog(dag_id, priority, "failed")
75
+
76
+
77
+ class DatadogPriorityPlugin(AirflowPlugin):
78
+ name = "DatadogPriorityPlugin"
79
+ listeners = [sys.modules[__name__]]
@@ -0,0 +1,55 @@
1
+ import sys
2
+ from functools import lru_cache
3
+
4
+ from airflow.listeners import hookimpl
5
+ from airflow.models.dagrun import DagRun
6
+ from airflow.plugins_manager import AirflowPlugin
7
+ from newrelic_telemetry_sdk import GaugeMetric, MetricClient
8
+
9
+ from airflow_priority import DagStatus, get_config_option, has_priority_tag
10
+
11
+ __all__ = (
12
+ "send_metric_newrelic",
13
+ "on_dag_run_running",
14
+ "on_dag_run_success",
15
+ "on_dag_run_failed",
16
+ "NewRelicPriorityPlugin",
17
+ )
18
+
19
+
20
+ @lru_cache
21
+ def get_client():
22
+ return MetricClient(get_config_option("newrelic", "api_key", "", True))
23
+
24
+
25
+ def send_metric_newrelic(dag_id: str, priority: int, tag: DagStatus) -> None:
26
+ priority = GaugeMetric(
27
+ f"airflow.custom.priority.p{priority}.{tag}", 1, tags={"application": "airflow", "dag": dag_id, "priority": priority, "tag": tag}
28
+ )
29
+ get_client().send_batch([priority])
30
+
31
+
32
+ @hookimpl
33
+ def on_dag_run_running(dag_run: DagRun, msg: str):
34
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
35
+ if priority:
36
+ send_metric_newrelic(dag_id, priority, "running")
37
+
38
+
39
+ @hookimpl
40
+ def on_dag_run_success(dag_run: DagRun, msg: str):
41
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
42
+ if priority:
43
+ send_metric_newrelic(dag_id, priority, "success")
44
+
45
+
46
+ @hookimpl
47
+ def on_dag_run_failed(dag_run: DagRun, msg: str):
48
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
49
+ if priority:
50
+ send_metric_newrelic(dag_id, priority, "failed")
51
+
52
+
53
+ class NewRelicPriorityPlugin(AirflowPlugin):
54
+ name = "NewRelicPriorityPlugin"
55
+ listeners = [sys.modules[__name__]]
@@ -0,0 +1,57 @@
1
+ import sys
2
+ from functools import lru_cache
3
+
4
+ from airflow.listeners import hookimpl
5
+ from airflow.models.dagrun import DagRun
6
+ from airflow.plugins_manager import AirflowPlugin
7
+ from slack_sdk import WebClient
8
+
9
+ from airflow_priority import DagStatus, get_config_option, has_priority_tag
10
+
11
+ __all__ = ("get_client", "get_channel_id", "send_metric_slack", "on_dag_run_failed", "SlackPriorityPlugin")
12
+
13
+
14
+ @lru_cache
15
+ def get_client():
16
+ return WebClient(token=get_config_option("slack", "token", ""))
17
+
18
+
19
+ @lru_cache
20
+ def get_channel_id():
21
+ channel_name = get_config_option("slack", "channel", "")
22
+ conversations = get_client().conversations_list(types=["public_channel", "private_channel"])
23
+ if conversations.data["ok"]:
24
+ for channel in conversations.data["channels"]:
25
+ if channel["name"] == channel_name:
26
+ return channel["id"]
27
+ raise Exception("TODO")
28
+
29
+
30
+ def send_metric_slack(dag_id: str, priority: int, tag: DagStatus) -> None:
31
+ get_client().chat_postMessage(channel=get_channel_id(), text=f'A P{priority} DAG "{dag_id}" has {tag}!')
32
+
33
+
34
+ # @hookimpl
35
+ # def on_dag_run_running(dag_run: DagRun, msg: str):
36
+ # dag_id, priority = has_priority_tag(dag_run=dag_run)
37
+ # if priority:
38
+ # send_metric_slack(dag_id, priority, "running")
39
+
40
+
41
+ # @hookimpl
42
+ # def on_dag_run_success(dag_run: DagRun, msg: str):
43
+ # dag_id, priority = has_priority_tag(dag_run=dag_run)
44
+ # if priority:
45
+ # send_metric_slack(dag_id, priority, "succeeded")
46
+
47
+
48
+ @hookimpl
49
+ def on_dag_run_failed(dag_run: DagRun, msg: str):
50
+ dag_id, priority = has_priority_tag(dag_run=dag_run)
51
+ if priority:
52
+ send_metric_slack(dag_id, priority, "failed")
53
+
54
+
55
+ class SlackPriorityPlugin(AirflowPlugin):
56
+ name = "SlackPriorityPlugin"
57
+ listeners = [sys.modules[__name__]]
@@ -0,0 +1,363 @@
1
+ {% raw %}
2
+ [core]
3
+ dags_folder = ~/airflow/dags
4
+ hostname_callable = airflow.utils.net.getfqdn
5
+ might_contain_dag_callable = airflow.utils.file.might_contain_dag_via_default_heuristic
6
+ default_timezone = utc
7
+ executor = SequentialExecutor
8
+ auth_manager = airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager
9
+ parallelism = 32
10
+ max_active_tasks_per_dag = 16
11
+ dags_are_paused_at_creation = True
12
+ max_active_runs_per_dag = 16
13
+ max_consecutive_failed_dag_runs_per_dag = 0
14
+ load_examples = True
15
+ plugins_folder = ~/airflow/plugins
16
+ execute_tasks_new_python_interpreter = False
17
+ fernet_key =
18
+ donot_pickle = True
19
+ dagbag_import_timeout = 30.0
20
+ dagbag_import_error_tracebacks = True
21
+ dagbag_import_error_traceback_depth = 2
22
+ dag_file_processor_timeout = 50
23
+ task_runner = StandardTaskRunner
24
+ default_impersonation =
25
+ security =
26
+ unit_test_mode = False
27
+ enable_xcom_pickling = False
28
+ allowed_deserialization_classes = airflow.*
29
+ allowed_deserialization_classes_regexp =
30
+ killed_task_cleanup_time = 60
31
+ dag_run_conf_overrides_params = True
32
+ dag_discovery_safe_mode = True
33
+ dag_ignore_file_syntax = regexp
34
+ default_task_retries = 0
35
+ default_task_retry_delay = 300
36
+ max_task_retry_delay = 86400
37
+ default_task_weight_rule = downstream
38
+ default_task_execution_timeout =
39
+ min_serialized_dag_update_interval = 30
40
+ compress_serialized_dags = False
41
+ min_serialized_dag_fetch_interval = 10
42
+ max_num_rendered_ti_fields_per_task = 30
43
+ check_slas = True
44
+ xcom_backend = airflow.models.xcom.BaseXCom
45
+ lazy_load_plugins = True
46
+ lazy_discover_providers = True
47
+ hide_sensitive_var_conn_fields = True
48
+ sensitive_var_conn_names =
49
+ default_pool_task_slot_count = 128
50
+ max_map_length = 1024
51
+ daemon_umask = 0o077
52
+ strict_dataset_uri_validation = False
53
+ database_access_isolation = False
54
+ test_connection = Disabled
55
+ max_templated_field_length = 4096
56
+
57
+ [database]
58
+ alembic_ini_file_path = alembic.ini
59
+ sql_alchemy_conn = sqlite:////~/airflow/airflow.db
60
+ sql_engine_encoding = utf-8
61
+ sql_alchemy_pool_enabled = True
62
+ sql_alchemy_pool_size = 5
63
+ sql_alchemy_max_overflow = 10
64
+ sql_alchemy_pool_recycle = 1800
65
+ sql_alchemy_pool_pre_ping = True
66
+ sql_alchemy_schema =
67
+ load_default_connections = True
68
+ max_db_retries = 3
69
+ check_migrations = True
70
+
71
+ [logging]
72
+ base_log_folder = ~/airflow/logs
73
+ remote_logging = False
74
+ remote_log_conn_id =
75
+ delete_local_logs = False
76
+ google_key_path =
77
+ remote_base_log_folder =
78
+ remote_task_handler_kwargs =
79
+ encrypt_s3_logs = False
80
+ logging_level = INFO
81
+ celery_logging_level =
82
+ fab_logging_level = WARNING
83
+ logging_config_class =
84
+ colored_console_log = True
85
+ colored_log_format = [%%(blue)s%%(asctime)s%%(reset)s] {%%(blue)s%%(filename)s:%%(reset)s%%(lineno)d} %%(log_color)s%%(levelname)s%%(reset)s - %%(log_color)s%%(message)s%%(reset)s
86
+ colored_formatter_class = airflow.utils.log.colored_log.CustomTTYColoredFormatter
87
+ log_format = [%%(asctime)s] {%%(filename)s:%%(lineno)d} %%(levelname)s - %%(message)s
88
+ simple_log_format = %%(asctime)s %%(levelname)s - %%(message)s
89
+ dag_processor_log_target = file
90
+ dag_processor_log_format = [%%(asctime)s] [SOURCE:DAG_PROCESSOR] {%%(filename)s:%%(lineno)d} %%(levelname)s - %%(message)s
91
+ log_formatter_class = airflow.utils.log.timezone_aware.TimezoneAware
92
+ secret_mask_adapter =
93
+ task_log_prefix_template =
94
+ log_filename_template = dag_id={{ ti.dag_id }}/run_id={{ ti.run_id }}/task_id={{ ti.task_id }}/{%% if ti.map_index >= 0 %%}map_index={{ ti.map_index }}/{%% endif %%}attempt={{ try_number }}.log
95
+ log_processor_filename_template = {{ filename }}.log
96
+ dag_processor_manager_log_location = ~/airflow/logs/dag_processor_manager/dag_processor_manager.log
97
+ dag_processor_manager_log_stdout = False
98
+ task_log_reader = task
99
+ extra_logger_names =
100
+ worker_log_server_port = 8793
101
+ trigger_log_server_port = 8794
102
+ file_task_handler_new_folder_permissions = 0o775
103
+ file_task_handler_new_file_permissions = 0o664
104
+ celery_stdout_stderr_separation = False
105
+ enable_task_context_logger = True
106
+
107
+ [metrics]
108
+ metrics_use_pattern_match = False
109
+ metrics_allow_list =
110
+ metrics_block_list =
111
+ statsd_on = False
112
+ statsd_host = localhost
113
+ statsd_port = 8125
114
+ statsd_prefix = airflow
115
+ stat_name_handler =
116
+ statsd_datadog_enabled = False
117
+ statsd_datadog_tags =
118
+ statsd_datadog_metrics_tags = True
119
+ statsd_disabled_tags = job_id,run_id
120
+ statsd_influxdb_enabled = False
121
+ otel_on = False
122
+ otel_host = localhost
123
+ otel_port = 8889
124
+ otel_prefix = airflow
125
+ otel_interval_milliseconds = 60000
126
+ otel_debugging_on = False
127
+ otel_ssl_active = False
128
+
129
+ [secrets]
130
+ backend =
131
+ backend_kwargs =
132
+ use_cache = False
133
+ cache_ttl_seconds = 900
134
+
135
+ [cli]
136
+ api_client = airflow.api.client.local_client
137
+ endpoint_url = http://localhost:8080
138
+
139
+ [debug]
140
+ fail_fast = False
141
+
142
+ [api]
143
+ enable_experimental_api = False
144
+ auth_backends = airflow.api.auth.backend.session
145
+ maximum_page_limit = 100
146
+ fallback_page_limit = 100
147
+ google_oauth2_audience =
148
+ google_key_path =
149
+ access_control_allow_headers =
150
+ access_control_allow_methods =
151
+ access_control_allow_origins =
152
+ enable_xcom_deserialize_support = False
153
+
154
+ [lineage]
155
+ backend =
156
+
157
+ [operators]
158
+ default_owner = airflow
159
+ default_deferrable = false
160
+ default_cpus = 1
161
+ default_ram = 512
162
+ default_disk = 512
163
+ default_gpus = 0
164
+ default_queue = default
165
+ allow_illegal_arguments = False
166
+
167
+ [webserver]
168
+ access_denied_message = Access is Denied
169
+ config_file = ~/airflow/webserver_config.py
170
+ base_url = http://localhost:8080
171
+ default_ui_timezone = UTC
172
+ web_server_host = 0.0.0.0
173
+ web_server_port = 8080
174
+ web_server_ssl_cert =
175
+ web_server_ssl_key =
176
+ session_backend = database
177
+ web_server_master_timeout = 120
178
+ web_server_worker_timeout = 120
179
+ worker_refresh_batch_size = 1
180
+ worker_refresh_interval = 6000
181
+ reload_on_plugin_change = False
182
+ secret_key = ANcdrL+FG0dj9+7o4K6LGA==
183
+ workers = 4
184
+ worker_class = sync
185
+ access_logfile = -
186
+ error_logfile = -
187
+ access_logformat =
188
+ expose_config = False
189
+ expose_hostname = False
190
+ expose_stacktrace = False
191
+ dag_default_view = grid
192
+ dag_orientation = LR
193
+ grid_view_sorting_order = topological
194
+ log_fetch_timeout_sec = 5
195
+ log_fetch_delay_sec = 2
196
+ log_auto_tailing_offset = 30
197
+ log_animation_speed = 1000
198
+ hide_paused_dags_by_default = False
199
+ page_size = 100
200
+ navbar_color = #fff
201
+ navbar_text_color = #51504f
202
+ navbar_hover_color = #eee
203
+ navbar_text_hover_color = #51504f
204
+ navbar_logo_text_color = #51504f
205
+ default_dag_run_display_number = 25
206
+ enable_proxy_fix = False
207
+ proxy_fix_x_for = 1
208
+ proxy_fix_x_proto = 1
209
+ proxy_fix_x_host = 1
210
+ proxy_fix_x_port = 1
211
+ proxy_fix_x_prefix = 1
212
+ cookie_secure = False
213
+ cookie_samesite = Lax
214
+ default_wrap = False
215
+ x_frame_enabled = True
216
+ show_recent_stats_for_completed_runs = True
217
+ session_lifetime_minutes = 43200
218
+ instance_name_has_markup = False
219
+ auto_refresh_interval = 3
220
+ warn_deployment_exposure = True
221
+ enable_swagger_ui = True
222
+ run_internal_api = False
223
+ caching_hash_method = md5
224
+ show_trigger_form_if_no_params = False
225
+ num_recent_configurations_for_trigger = 5
226
+ allow_raw_html_descriptions = False
227
+ allowed_payload_size = 1.0
228
+ require_confirmation_dag_change = False
229
+
230
+ [email]
231
+ email_backend = airflow.utils.email.send_email_smtp
232
+ email_conn_id = smtp_default
233
+ default_email_on_retry = True
234
+ default_email_on_failure = True
235
+ ssl_context = default
236
+
237
+ [smtp]
238
+ smtp_host = localhost
239
+ smtp_starttls = True
240
+ smtp_ssl = False
241
+ smtp_port = 25
242
+ smtp_mail_from = airflow@example.com
243
+ smtp_timeout = 30
244
+ smtp_retry_limit = 5
245
+
246
+ [sentry]
247
+ sentry_on = false
248
+ sentry_dsn =
249
+
250
+ [scheduler]
251
+ job_heartbeat_sec = 5
252
+ scheduler_heartbeat_sec = 5
253
+ local_task_job_heartbeat_sec = 0
254
+ num_runs = -1
255
+ scheduler_idle_sleep_time = 1
256
+ min_file_process_interval = 30
257
+ parsing_cleanup_interval = 60
258
+ stale_dag_threshold = 50
259
+ dag_dir_list_interval = 300
260
+ print_stats_interval = 30
261
+ pool_metrics_interval = 5.0
262
+ scheduler_health_check_threshold = 30
263
+ enable_health_check = False
264
+ scheduler_health_check_server_host = 0.0.0.0
265
+ scheduler_health_check_server_port = 8974
266
+ orphaned_tasks_check_interval = 300.0
267
+ child_process_log_directory = ~/airflow/logs/scheduler
268
+ scheduler_zombie_task_threshold = 300
269
+ zombie_detection_interval = 10.0
270
+ catchup_by_default = True
271
+ ignore_first_depends_on_past_by_default = True
272
+ max_tis_per_query = 16
273
+ use_row_level_locking = True
274
+ max_dagruns_to_create_per_loop = 10
275
+ max_dagruns_per_loop_to_schedule = 20
276
+ schedule_after_task_execution = True
277
+ parsing_pre_import_modules = True
278
+ parsing_processes = 2
279
+ file_parsing_sort_mode = modified_time
280
+ standalone_dag_processor = False
281
+ max_callbacks_per_loop = 20
282
+ dag_stale_not_seen_duration = 600
283
+ use_job_schedule = True
284
+ allow_trigger_in_future = False
285
+ trigger_timeout_check_interval = 15
286
+ task_queued_timeout = 600.0
287
+ task_queued_timeout_check_interval = 120.0
288
+ allowed_run_id_pattern = ^[A-Za-z0-9_.~:+-]+$
289
+ create_cron_data_intervals = True
290
+
291
+ [triggerer]
292
+ default_capacity = 1000
293
+ job_heartbeat_sec = 5
294
+ triggerer_health_check_threshold = 30
295
+
296
+ [kerberos]
297
+ ccache = /tmp/airflow_krb5_ccache
298
+ principal = airflow
299
+ reinit_frequency = 3600
300
+ kinit_path = kinit
301
+ keytab = airflow.keytab
302
+ forwardable = True
303
+ include_ip = True
304
+
305
+ [sensors]
306
+ default_timeout = 604800
307
+
308
+ [celery_kubernetes_executor]
309
+ kubernetes_queue = kubernetes
310
+
311
+ [celery]
312
+ celery_app_name = airflow.providers.celery.executors.celery_executor
313
+ worker_concurrency = 16
314
+ worker_prefetch_multiplier = 1
315
+ worker_enable_remote_control = true
316
+ broker_url = redis://redis:6379/0
317
+ result_backend_sqlalchemy_engine_options =
318
+ flower_host = 0.0.0.0
319
+ flower_url_prefix =
320
+ flower_port = 5555
321
+ flower_basic_auth =
322
+ sync_parallelism = 0
323
+ celery_config_options = airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG
324
+ ssl_active = False
325
+ ssl_key =
326
+ ssl_cert =
327
+ ssl_cacert =
328
+ pool = prefork
329
+ operation_timeout = 1.0
330
+ task_acks_late = True
331
+ task_track_started = True
332
+ task_publish_max_retries = 3
333
+ worker_precheck = False
334
+
335
+ [celery_broker_transport_options]
336
+
337
+ [common.io]
338
+ xcom_objectstorage_path =
339
+ xcom_objectstorage_threshold = -1
340
+ xcom_objectstorage_compression =
341
+
342
+ [fab]
343
+ auth_rate_limited = True
344
+ auth_rate_limit = 5 per 40 second
345
+ update_fab_perms = True
346
+
347
+ [imap]
348
+ [smtp_provider]
349
+ [atlas]
350
+ test = "blerg"
351
+ [airflow.priority.slack]
352
+ test="blerg"
353
+ {% endraw %}
354
+
355
+ [priority.slack]
356
+ token = {{ SLACK_TOKEN }}
357
+ channel = {{ SLACK_CHANNEL }}
358
+
359
+ [priority.datadog]
360
+ api_key = {{ DATADOG_API_KEY }}
361
+
362
+ [priority.newrelic]
363
+ api_key = {{ NEWRELIC_API_KEY }}
@@ -0,0 +1,44 @@
1
+ import os
2
+ from pathlib import Path
3
+ from tempfile import TemporaryDirectory
4
+
5
+ import pytest
6
+ from jinja2 import DictLoader, Environment
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def airflow_config():
11
+ config_template = (Path(__file__).parent / "airflow.cfg.jinja").read_text()
12
+ j2 = Environment(loader=DictLoader({"airflow.cfg": config_template}), trim_blocks=True)
13
+ with TemporaryDirectory() as td:
14
+ tmpl = j2.get_template("airflow.cfg").render(
15
+ SLACK_TOKEN=os.environ.get("SLACK_TOKEN", ""),
16
+ SLACK_CHANNEL=os.environ.get("SLACK_CHANNEL", ""),
17
+ DATADOG_HOST=os.environ.get("DATADOG_HOST", ""),
18
+ DATADOG_API_KEY=os.environ.get("DATADOG_API_KEY", ""),
19
+ NEWRELIC_API_KEY=os.environ.get("NEWRELIC_API_KEY", ""),
20
+ )
21
+ (Path(td) / "airflow.cfg").write_text(tmpl)
22
+ os.environ["AIRFLOW_HOME"] = str(Path(td))
23
+ os.environ["AIRFLOW_CONFIG"] = str((Path(td) / "airflow.cfg"))
24
+ import airflow.configuration
25
+
26
+ airflow.configuration.AIRFLOW_HOME = os.environ["AIRFLOW_HOME"]
27
+ airflow.configuration.AIRFLOW_CONFIG = os.environ["AIRFLOW_CONFIG"]
28
+ airflow_config_parser = airflow.configuration.AirflowConfigParser()
29
+ airflow.configuration.load_standard_airflow_configuration(airflow_config_parser)
30
+ airflow_config_parser.validate()
31
+ airflow.configuration.conf = airflow_config_parser
32
+ yield
33
+
34
+
35
+ @pytest.fixture(scope="function", autouse=True)
36
+ def dag_run():
37
+ from unittest.mock import MagicMock
38
+
39
+ from airflow.models.dagrun import DagRun
40
+
41
+ dag_run = DagRun("UNIT TEST")
42
+ dag_run.dag = MagicMock()
43
+ dag_run.dag.tags = ["P1"]
44
+ return dag_run
@@ -0,0 +1,4 @@
1
+ def test_has_priority_tag(dag_run):
2
+ from airflow_priority import has_priority_tag
3
+
4
+ assert has_priority_tag(dag_run) == ("UNIT TEST", 1)
@@ -0,0 +1,31 @@
1
+ from unittest.mock import patch
2
+
3
+
4
+ def test_datadog_send(airflow_config, dag_run):
5
+ from airflow_priority.plugins.datadog import send_metric_datadog
6
+
7
+ send_metric_datadog("UNIT TEST", 1, "testing")
8
+
9
+
10
+ def test_datadog_priority_failed(airflow_config, dag_run):
11
+ from airflow_priority.plugins.datadog import on_dag_run_failed
12
+
13
+ with patch("airflow_priority.plugins.datadog.send_metric_datadog") as p1:
14
+ on_dag_run_failed(dag_run, "test")
15
+ assert p1.call_count == 1
16
+
17
+
18
+ def test_datadog_priority_running(airflow_config, dag_run):
19
+ from airflow_priority.plugins.datadog import on_dag_run_running
20
+
21
+ with patch("airflow_priority.plugins.datadog.send_metric_datadog") as p1:
22
+ on_dag_run_running(dag_run, "test")
23
+ assert p1.call_count == 1
24
+
25
+
26
+ def test_datadog_priority_success(airflow_config, dag_run):
27
+ from airflow_priority.plugins.datadog import on_dag_run_success
28
+
29
+ with patch("airflow_priority.plugins.datadog.send_metric_datadog") as p1:
30
+ on_dag_run_success(dag_run, "test")
31
+ assert p1.call_count == 1
@@ -0,0 +1,31 @@
1
+ from unittest.mock import patch
2
+
3
+
4
+ def test_newrelic_send(airflow_config, dag_run):
5
+ from airflow_priority.plugins.newrelic import send_metric_newrelic
6
+
7
+ send_metric_newrelic("UNIT TEST", 1, "testing")
8
+
9
+
10
+ def test_newrelic_priority_failed(airflow_config, dag_run):
11
+ from airflow_priority.plugins.newrelic import on_dag_run_failed
12
+
13
+ with patch("airflow_priority.plugins.newrelic.send_metric_newrelic") as p1:
14
+ on_dag_run_failed(dag_run, "test")
15
+ assert p1.call_count == 1
16
+
17
+
18
+ def test_newrelic_priority_running(airflow_config, dag_run):
19
+ from airflow_priority.plugins.newrelic import on_dag_run_running
20
+
21
+ with patch("airflow_priority.plugins.newrelic.send_metric_newrelic") as p1:
22
+ on_dag_run_running(dag_run, "test")
23
+ assert p1.call_count == 1
24
+
25
+
26
+ def test_newrelic_priority_success(airflow_config, dag_run):
27
+ from airflow_priority.plugins.newrelic import on_dag_run_success
28
+
29
+ with patch("airflow_priority.plugins.newrelic.send_metric_newrelic") as p1:
30
+ on_dag_run_success(dag_run, "test")
31
+ assert p1.call_count == 1
@@ -0,0 +1,15 @@
1
+ from unittest.mock import patch
2
+
3
+
4
+ def test_slack_send(airflow_config, dag_run):
5
+ from airflow_priority.plugins.slack import send_metric_slack
6
+
7
+ send_metric_slack("UNIT TEST", 1, "BEEN TESTED")
8
+
9
+
10
+ def test_slack_priority_failed(airflow_config, dag_run):
11
+ from airflow_priority.plugins.slack import on_dag_run_failed
12
+
13
+ with patch("airflow_priority.plugins.slack.send_metric_slack") as p1:
14
+ on_dag_run_failed(dag_run, "test")
15
+ assert p1.call_count == 1
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.3
2
+ Name: airflow-priority
3
+ Version: 0.1.0
4
+ Summary: Priority Tags for Airflow Dags
5
+ Project-URL: Repository, https://github.com/airflow-laminar/airflow-priority
6
+ Project-URL: Homepage, https://github.com/airflow-laminar/airflow-priority
7
+ Author-email: the airflow-priority authors <3105306+timkpaine@users.noreply.github.com>
8
+ License: Apache-2.0
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: apache-airflow<3,>=2
21
+ Provides-Extra: aws
22
+ Requires-Dist: boto3; extra == 'aws'
23
+ Requires-Dist: botocore; extra == 'aws'
24
+ Provides-Extra: datadog
25
+ Requires-Dist: datadog-api-client; extra == 'datadog'
26
+ Provides-Extra: develop
27
+ Requires-Dist: boto3; extra == 'develop'
28
+ Requires-Dist: botocore; extra == 'develop'
29
+ Requires-Dist: build; extra == 'develop'
30
+ Requires-Dist: bump-my-version; extra == 'develop'
31
+ Requires-Dist: check-manifest; extra == 'develop'
32
+ Requires-Dist: datadog-api-client; extra == 'develop'
33
+ Requires-Dist: hatchling; extra == 'develop'
34
+ Requires-Dist: newrelic-telemetry-sdk; extra == 'develop'
35
+ Requires-Dist: pytest; extra == 'develop'
36
+ Requires-Dist: pytest-cov; extra == 'develop'
37
+ Requires-Dist: ruff; extra == 'develop'
38
+ Requires-Dist: slack-sdk; extra == 'develop'
39
+ Requires-Dist: twine; extra == 'develop'
40
+ Requires-Dist: wheel; extra == 'develop'
41
+ Provides-Extra: newrelic
42
+ Requires-Dist: newrelic-telemetry-sdk; extra == 'newrelic'
43
+ Provides-Extra: slack
44
+ Requires-Dist: slack-sdk; extra == 'slack'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # airflow-priority
48
+
49
+ Priority Tags for Airflow Dags
50
+
51
+ [![Build Status](https://github.com/airflow-laminar/airflow-priority/actions/workflows/build.yml/badge.svg?branch=main&event=push)](https://github.com/airflow-laminar/airflow-priority/actions/workflows/build.yml)
52
+ [![codecov](https://codecov.io/gh/airflow-laminar/airflow-priority/branch/main/graph/badge.svg)](https://codecov.io/gh/airflow-laminar/airflow-priority)
53
+ [![License](https://img.shields.io/github/license/airflow-laminar/airflow-priority)](https://github.com/airflow-laminar/airflow-priority)
54
+ [![PyPI](https://img.shields.io/pypi/v/airflow-priority.svg)](https://pypi.python.org/pypi/airflow-priority)
55
+
56
+ ## Overview
57
+
58
+ This repo provides [Airflow Plugins](https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/plugins.html) for priority-driven DAG failure alerting. In layman's terms, one need only add a [tag](https://airflow.apache.org/docs/apache-airflow/stable/howto/add-dag-tags.html) to their DAG in `P1, P2, P3, P4, P5`, and that dag will send a notification to:
59
+
60
+ - [New Relic](https://newrelic.com)
61
+ - [Datadog](https://www.datadoghq.com)
62
+ - [Slack](http://slack.com)
63
+
64
+ Where `P1` corresponds to highest priority, and `P5` corresponds to lowest.
65
+
66
+ ## Installation
67
+
68
+ You can install from pip:
69
+
70
+ ```bash
71
+ pip install airflow-priority
72
+ ```
73
+
74
+ Or via conda:
75
+
76
+ ```bash
77
+ conda install airflow-priority -c conda-forge
78
+ ```
79
+
80
+ ## Integrations
81
+ | Integration | Metric / Tag |
82
+ | :---------- | :----------- |
83
+ | [New Relic](https://newrelic.com) | `airflow.custom.priority.p{1,2,3,4,5}.{failed,succeeded,running}` |
84
+ | [Datadog](https://www.datadoghq.com) | `airflow.custom.priority.p{1,2,3,4,5}.{failed,succeeded,running}` |
85
+ | [Slack](http://slack.com) | `N/A` |
86
+
87
+
88
+ ### New Relic
89
+
90
+ Create a new New Relic API Key [following their guide](https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/). Note that the type should have `INGEST - LICENSE`.
91
+
92
+ Copy this api key into your `airflow.cfg` like so:
93
+
94
+ ```
95
+ [priority.newrelic]
96
+ api_key = the api key
97
+ ```
98
+
99
+ Under `Query Your Data` in the New Relic UI, you can create a query for the new custom metric:
100
+
101
+ ```
102
+ SELECT sum(`airflow.custom.priority.p1.failed`) FROM Metric FACET dag
103
+ ```
104
+
105
+ With this, you can now [create a custom alert](https://docs.newrelic.com/docs/alerts/create-alert/examples/define-custom-metrics-alert-condition/). For fast alerting, we recommend the following parameters:
106
+
107
+ ```raw
108
+ Window duration - 30 seconds
109
+ Sliding window aggregation - Disabled
110
+ Slide by interval - Not set
111
+ Streaming method - Event timer
112
+ Timer - 5 seconds
113
+
114
+ Fill data gaps with - None
115
+ Evaluation delay - Not set
116
+
117
+ Thresholds: Critical: Query result is above or equals 1 at least once in 1 minute
118
+ ```
119
+
120
+ ### Slack
121
+
122
+ Configure a new slack application following the [Slack Quickstart](https://api.slack.com/quickstart).
123
+
124
+ Ensure your application has the following scopes for public and private channel access:
125
+
126
+ - `channels:read`
127
+ - `groups:read`
128
+ - `chat:write`
129
+
130
+ Enable and install your Slack application into your workspace, and add it as an integration in whatever channel you want it to post.
131
+
132
+ Copy your Slack application's Oauth Token (starting with `xoxb-`) and your desired channel into your `airflow.cfg` like so:
133
+
134
+ ```
135
+ [priority.slack]
136
+ token = xoxb-...
137
+ channel = channel-name
138
+ ```
139
+
140
+ ### Datadog
141
+
142
+ Create a new Datadog api key [following their guide](https://docs.datadoghq.com/account_management/api-app-keys/#add-an-api-key-or-client-token).
143
+
144
+ Copy this api key into your `airflow.cfg` like so:
145
+
146
+ ```
147
+ [priority.datadog]
148
+ api_key = the api key
149
+ ```
150
+
151
+ Ensure your dags are configured with tags and run some, it can often be convenient to have an intentionally failing `P1` dag to test the integration. With this, you can now [create custom monitors](https://docs.datadoghq.com/getting_started/monitors/) for the tags.
152
+
153
+
154
+ ## License
@@ -0,0 +1,18 @@
1
+ airflow_priority/__init__.py,sha256=JRAlq9ECAesJA0mEaoYsdF-a81-p96Ne327m2Bz5cPo,45
2
+ airflow_priority/common.py,sha256=kOUXLY6wBp2iyKafjiYDWqcq5OxpkJu-t4eaEi4y5ro,1175
3
+ airflow_priority/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ airflow_priority/plugins/aws.py,sha256=waws2w3Zr7Sd-KPdHh0euqDcQ6ALZbhSkg1zhO4m-38,1960
5
+ airflow_priority/plugins/datadog.py,sha256=3Twjn-cqsUcfGIxcO6Tcrg2-dRyI8hxTirMiC27B4O8,2620
6
+ airflow_priority/plugins/newrelic.py,sha256=u7K0jPsTXeGzepsvxJoyhy8wRoRkQ-xvw0OllS6oTG4,1567
7
+ airflow_priority/plugins/slack.py,sha256=dqMHKheBWn63tgvsOYtdlIpARnzzKcQPCGm4h88t8K4,1784
8
+ airflow_priority/tests/airflow.cfg.jinja,sha256=0NFCaKzB4y-SfdcU01Okzsn5yu9Wolc_rtiEFwmsMQk,9985
9
+ airflow_priority/tests/conftest.py,sha256=mUl_LWdzbI4Tfvjg66aqZCBsv9BoQbygGGeNC1mLcLE,1683
10
+ airflow_priority/tests/test_common.py,sha256=1ctwps0n4VmZJFm6eqiYcHyyd-jnf3sXaTnur_zwAz4,144
11
+ airflow_priority/tests/test_datadog.py,sha256=ly-NbB3yAmS5KHN53e5odInzaHgGdqOpocZ-md4CV_8,1049
12
+ airflow_priority/tests/test_newrelic.py,sha256=heEc_MV_cYT0dumg3ChYDa5MPNZuOGUGUoxre1AD8oo,1065
13
+ airflow_priority/tests/test_slack.py,sha256=tZvYVMQ-aYOuZbYyPa4A9DkaYJvgccsZaHHVb8NUe9s,471
14
+ airflow_priority-0.1.0.dist-info/METADATA,sha256=cC3_1Od7GcspN2HdzPx4K__SDvfQS7mOmpoonFKM-5k,5775
15
+ airflow_priority-0.1.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
16
+ airflow_priority-0.1.0.dist-info/entry_points.txt,sha256=rANAxUPYmf0fI8I2YZSsDyeEZeAfknp6wz7P75CV3uI,352
17
+ airflow_priority-0.1.0.dist-info/licenses/LICENSE,sha256=SmmTABBMCdZmZ8-nO_YaPUSw6Hvd8SD2kptj4jEO7SE,11358
18
+ airflow_priority-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [airflow.plugins]
2
+ airflow_priority_aws_cloudwatch = airflow_priority.plugins.aws:AWSCloudWatchPriorityPlugin
3
+ airflow_priority_datadog = airflow_priority.plugins.datadog:DatadogPriorityPlugin
4
+ airflow_priority_newrelic = airflow_priority.plugins.newrelic:NewRelicPriorityPlugin
5
+ airflow_priority_slack = airflow_priority.plugins.slack:SlackPriorityPlugin
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2024 the airflow-priority authors
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.