airflow-plugin-watchdog 0.6.0__tar.gz

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 (43) hide show
  1. airflow_plugin_watchdog-0.6.0/.github/workflows/integration.yml +72 -0
  2. airflow_plugin_watchdog-0.6.0/.github/workflows/lint.yml +36 -0
  3. airflow_plugin_watchdog-0.6.0/.github/workflows/publish.yml +67 -0
  4. airflow_plugin_watchdog-0.6.0/.github/workflows/test.yml +48 -0
  5. airflow_plugin_watchdog-0.6.0/.gitignore +16 -0
  6. airflow_plugin_watchdog-0.6.0/CHANGELOG.md +131 -0
  7. airflow_plugin_watchdog-0.6.0/LICENSE +191 -0
  8. airflow_plugin_watchdog-0.6.0/PKG-INFO +294 -0
  9. airflow_plugin_watchdog-0.6.0/README.md +262 -0
  10. airflow_plugin_watchdog-0.6.0/airflow_watchdog/__init__.py +24 -0
  11. airflow_plugin_watchdog-0.6.0/airflow_watchdog/alerting.py +232 -0
  12. airflow_plugin_watchdog-0.6.0/airflow_watchdog/config.py +126 -0
  13. airflow_plugin_watchdog-0.6.0/airflow_watchdog/detectors/__init__.py +44 -0
  14. airflow_plugin_watchdog-0.6.0/airflow_watchdog/detectors/_stats.py +53 -0
  15. airflow_plugin_watchdog-0.6.0/airflow_watchdog/detectors/deadlines.py +141 -0
  16. airflow_plugin_watchdog-0.6.0/airflow_watchdog/detectors/failures.py +142 -0
  17. airflow_plugin_watchdog-0.6.0/airflow_watchdog/detectors/runtime.py +135 -0
  18. airflow_plugin_watchdog-0.6.0/airflow_watchdog/detectors/schedule.py +183 -0
  19. airflow_plugin_watchdog-0.6.0/airflow_watchdog/detectors/stuck.py +141 -0
  20. airflow_plugin_watchdog-0.6.0/airflow_watchdog/monitor.py +164 -0
  21. airflow_plugin_watchdog-0.6.0/airflow_watchdog/plugin.py +24 -0
  22. airflow_plugin_watchdog-0.6.0/airflow_watchdog/py.typed +0 -0
  23. airflow_plugin_watchdog-0.6.0/airflow_watchdog/scheduler.py +205 -0
  24. airflow_plugin_watchdog-0.6.0/airflow_watchdog/ui/__init__.py +0 -0
  25. airflow_plugin_watchdog-0.6.0/airflow_watchdog/ui/app.py +367 -0
  26. airflow_plugin_watchdog-0.6.0/airflow_watchdog/ui/templates/config.html +472 -0
  27. airflow_plugin_watchdog-0.6.0/airflow_watchdog/ui/templates/dashboard.html +381 -0
  28. airflow_plugin_watchdog-0.6.0/prek.toml +29 -0
  29. airflow_plugin_watchdog-0.6.0/pyproject.toml +109 -0
  30. airflow_plugin_watchdog-0.6.0/tests/integration/__init__.py +0 -0
  31. airflow_plugin_watchdog-0.6.0/tests/integration/_seed.py +109 -0
  32. airflow_plugin_watchdog-0.6.0/tests/integration/conftest.py +91 -0
  33. airflow_plugin_watchdog-0.6.0/tests/integration/test_integration.py +251 -0
  34. airflow_plugin_watchdog-0.6.0/tests/unit/__init__.py +0 -0
  35. airflow_plugin_watchdog-0.6.0/tests/unit/test_alerting.py +221 -0
  36. airflow_plugin_watchdog-0.6.0/tests/unit/test_alerts.py +41 -0
  37. airflow_plugin_watchdog-0.6.0/tests/unit/test_config.py +86 -0
  38. airflow_plugin_watchdog-0.6.0/tests/unit/test_dashboard.py +434 -0
  39. airflow_plugin_watchdog-0.6.0/tests/unit/test_detectors.py +596 -0
  40. airflow_plugin_watchdog-0.6.0/tests/unit/test_monitor.py +123 -0
  41. airflow_plugin_watchdog-0.6.0/tests/unit/test_plugin.py +25 -0
  42. airflow_plugin_watchdog-0.6.0/tests/unit/test_scheduler.py +99 -0
  43. airflow_plugin_watchdog-0.6.0/uv.lock +3622 -0
@@ -0,0 +1,72 @@
1
+ name: Integration Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "airflow_watchdog/**"
8
+ - "tests/integration/**"
9
+ - "pyproject.toml"
10
+ - "uv.lock"
11
+ - ".github/workflows/integration.yml"
12
+ pull_request:
13
+ branches: [main]
14
+ paths:
15
+ - "airflow_watchdog/**"
16
+ - "tests/integration/**"
17
+ - "pyproject.toml"
18
+ - "uv.lock"
19
+ - ".github/workflows/integration.yml"
20
+
21
+ concurrency:
22
+ group: ${{ github.workflow }}-${{ github.ref }}
23
+ cancel-in-progress: true
24
+
25
+ permissions:
26
+ contents: read
27
+
28
+ jobs:
29
+ integration:
30
+ runs-on: ubuntu-latest
31
+ strategy:
32
+ fail-fast: false
33
+ matrix:
34
+ # PostgreSQL is the production backend; SQLite is exercised too because
35
+ # the watchdog reads timestamps/JSON via raw SQL, which differs by driver.
36
+ include:
37
+ - backend: postgres
38
+ db_url: "postgresql+psycopg2://airflow:airflow@localhost:5432/airflow"
39
+ - backend: sqlite
40
+ db_url: "sqlite:////tmp/watchdog_integration.db"
41
+
42
+ services:
43
+ postgres:
44
+ image: postgres:16
45
+ env:
46
+ POSTGRES_USER: airflow
47
+ POSTGRES_PASSWORD: airflow
48
+ POSTGRES_DB: airflow
49
+ ports:
50
+ - 5432:5432
51
+ options: >-
52
+ --health-cmd "pg_isready -U airflow"
53
+ --health-interval 10s
54
+ --health-timeout 5s
55
+ --health-retries 5
56
+
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+
60
+ - name: Install uv
61
+ uses: astral-sh/setup-uv@v5
62
+
63
+ - name: Create the virtualenv on Python 3.14
64
+ run: uv venv .venv --python 3.14
65
+
66
+ - name: Install dependencies (incl. DB drivers)
67
+ run: uv sync --extra dev
68
+
69
+ - name: Run integration tests (${{ matrix.backend }})
70
+ env:
71
+ WATCHDOG_IT_DB_URL: ${{ matrix.db_url }}
72
+ run: uv run --extra dev pytest tests/integration -m integration -v
@@ -0,0 +1,36 @@
1
+ name: Lint
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ lint-and-format:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - name: Checkout code
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v5
22
+
23
+ - name: Check lockfile is up to date
24
+ run: uv lock --check
25
+
26
+ - name: Run ruff lint
27
+ run: uvx ruff check --extend-select=I
28
+
29
+ - name: Run ruff format
30
+ run: uvx ruff format --check --diff
31
+
32
+ - name: Install dependencies
33
+ run: uv sync
34
+
35
+ - name: Run type check
36
+ run: uvx ty check airflow_watchdog
@@ -0,0 +1,67 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.14"
20
+
21
+ - name: Install build dependencies
22
+ run: pip install build
23
+
24
+ - name: Build package
25
+ run: python -m build
26
+
27
+ - name: Upload build artifacts
28
+ uses: actions/upload-artifact@v4
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+
33
+ publish:
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment: pypi
37
+ permissions:
38
+ id-token: write
39
+ steps:
40
+ - name: Download build artifacts
41
+ uses: actions/download-artifact@v4
42
+ with:
43
+ name: dist
44
+ path: dist/
45
+
46
+ - name: Publish to PyPI
47
+ uses: pypa/gh-action-pypi-publish@release/v1
48
+
49
+ verify:
50
+ needs: publish
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+
55
+ - name: Set up Python
56
+ uses: actions/setup-python@v5
57
+ with:
58
+ python-version: "3.14"
59
+
60
+ - name: Install tox
61
+ run: pip install tox
62
+
63
+ - name: Wait for PyPI to update
64
+ run: sleep 30
65
+
66
+ - name: Run tests against published package
67
+ run: tox -e verify-airflow3
@@ -0,0 +1,48 @@
1
+ name: Unit Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
23
+
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - name: Install uv
28
+ uses: astral-sh/setup-uv@v5
29
+
30
+ - name: Set up Python ${{ matrix.python-version }}
31
+ run: uv python install ${{ matrix.python-version }}
32
+
33
+ - name: Install tox
34
+ run: |
35
+ uv venv .venv --python ${{ matrix.python-version }}
36
+ uv pip install tox tox-uv --python .venv/bin/python
37
+
38
+ - name: Run tests
39
+ run: .venv/bin/tox -e airflow3
40
+
41
+ - name: Upload coverage to Codecov
42
+ if: matrix.python-version == '3.14'
43
+ uses: codecov/codecov-action@v4
44
+ with:
45
+ files: coverage.xml
46
+ fail_ci_if_error: false
47
+ env:
48
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .venv/
12
+ venv/
13
+ *.so
14
+ .DS_Store
15
+ .claude/
16
+ .idea/
@@ -0,0 +1,131 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.6.0] - 2026-05-31
9
+
10
+ > **Renamed:** the distribution is now **`airflow-plugin-watchdog`** (was `airflow-provider-watchdog`). Install with `pip install airflow-plugin-watchdog`. The import path is unchanged (`import airflow_watchdog`).
11
+
12
+ ### Fixed
13
+
14
+ - **`RuntimeError: Direct database access via the ORM is not allowed in Airflow 3.0`** — the monitor DAG's task ran the detector SQL through `airflow.settings.Session`, but Airflow 3 isolates task/worker execution from the metadata DB (AIP-72), so every run failed. Detection now runs on the API-server side, where direct metadata-DB access is sanctioned.
15
+
16
+ ### Changed
17
+
18
+ - **Detection moved off the worker and onto the API server.** A background scheduler, started by the plugin's FastAPI lifespan, runs the detectors every `schedule_interval_minutes` inside the API-server process — the same place, and the same DB access, the dashboard already used. The synchronous detector SQL runs on a dedicated daemon thread so it never blocks the API server's event loop. Across multiple API-server replicas/workers, a database advisory lock (Postgres/MySQL) plus a last-run check ensures only one cycle runs per interval.
19
+ - **No monitoring DAG to deploy.** Installing the plugin is now sufficient; there is no `dags_folder` shim step. `schedule_interval_minutes` is read each cycle, so cadence changes apply without a restart (previously required a scheduler restart).
20
+ - Dashboard alert results now come from the `watchdog_last_results` Variable (written by the scheduler, capped at 50 most-severe alerts) instead of the monitor DAG's XCom.
21
+ - `exclude_dags` no longer force-includes the (now-removed) `airflow_watchdog_monitor` DAG; it defaults to `[]` and is normalized to a sorted, de-duplicated list.
22
+
23
+ ### Removed
24
+
25
+ - The `airflow_watchdog_monitor` DAG (`airflow_watchdog/dag.py`) and the `example_dags/watchdog_monitor.py` shim. Detection no longer runs as a DAG task, so it no longer appears in Airflow's DAG/run list — its activity is visible in the dashboard and the API-server logs.
26
+ - **Provider registration.** The package is now a plain Airflow **plugin**, not a provider: `get_provider_info()` and the `apache_airflow_provider` entry point are gone. The plugin loads via the `airflow.plugins` entry point (the registration that already did the work). It no longer appears in Airflow's Providers list — look under Plugins instead.
27
+
28
+ ## [0.5.0] - 2026-05-30
29
+
30
+ ### Fixed
31
+
32
+ - **Dashboard / nav link never appeared** — `WatchdogPlugin` was never registered, so the `/watchdog/` dashboard and the Browse → Watchdog nav link were invisible in every deployment. The plugin is now registered both via the provider-info `plugins` key and an `airflow.plugins` entry point (the latter survives `LAZY_LOAD_PROVIDERS=True`). Airflow dedupes by plugin name, so the two registrations don't double-mount.
33
+ - **`"dags"` provider-info key did nothing** — the key added in 0.4.1 is not part of Airflow's provider-info schema and was silently ignored; Airflow has no mechanism to auto-discover DAGs shipped inside provider packages. Removed it.
34
+
35
+ ### Added
36
+
37
+ - `example_dags/watchdog_monitor.py` — a one-line shim that re-exports the monitor DAG. Copy it into your `dags_folder` to expose `airflow_watchdog_monitor` to the scheduler (see README).
38
+ - Provider-wiring integration tests asserting the plugin is discoverable via `ProvidersManager` and the monitor DAG loads cleanly in a `DagBag`.
39
+
40
+ ### Changed
41
+
42
+ - README installation steps now reflect reality: `pip install` registers the dashboard, and the monitor DAG requires dropping the shim into `dags_folder`.
43
+
44
+ ## [0.4.1] - 2026-05-30
45
+
46
+ ### Fixed
47
+
48
+ - **DAG not auto-discovered after install** — added missing `"dags"` key to provider info so Airflow's provider manager correctly discovers the `airflow_watchdog_monitor` DAG (superseded by 0.5.0 — this key is not a real Airflow mechanism and had no effect)
49
+
50
+ ## [0.4.0] - 2026-05-29
51
+
52
+ ### Added
53
+
54
+ - Python 3.14 support (added to classifiers and CI test matrix)
55
+ - Integration test suite that runs the detector/dashboard SQL, the XCom round trip, and the auth dependencies against a **real** Airflow metadata DB; CI exercises it on a PostgreSQL service container and on SQLite
56
+
57
+ ### Security
58
+
59
+ - Dashboard and config endpoints now require an authenticated Airflow user. Reads require website (view) access; saving config requires permission to edit Airflow Variables. Previously these plugin endpoints — including the config-write endpoint that mutates the `watchdog_config` Variable — were reachable without authentication.
60
+
61
+ ### Fixed
62
+
63
+ - **Deadline/stuck/schedule detectors and dashboard broken on SQLite** — raw SQL returns timestamps as strings on SQLite (vs `datetime` objects on PostgreSQL/MySQL), so duration arithmetic raised `TypeError`. Timestamps are now coerced via a shared `as_datetime` helper that works on every backend.
64
+ - **Dashboard showed no alerts** — the watchdog DAG pushed `json.dumps(summary)` to XCom, which XCom serialized again (double-encoding); the dashboard's `json.loads` then yielded a string instead of a dict. The DAG now pushes the dict directly, and the dashboard decodes defensively across backends (handles single/double-encoded strings and pre-decoded JSON).
65
+ - Failure-spike baseline now **excludes** the recent window (`rn > window`), so a fresh wave of failures no longer dilutes the baseline it's compared against
66
+ - Naive metadata timestamps are now interpreted as UTC (the value Airflow actually stores) instead of `airflow.settings.TIMEZONE`, which produced incorrect elapsed times for deadline/stuck detection on non-UTC deployments
67
+ - Dashboard DAG-link `base_url` is now passed through `encodeURI` for consistent escaping
68
+
69
+ ### Changed
70
+
71
+ - Alerts serialized into the dashboard XCom are capped (most-severe first) to keep the payload bounded; `total_alerts`/`by_type` counts still reflect every alert
72
+
73
+ ## [0.3.1] - 2026-03-25
74
+
75
+ ### Fixed
76
+
77
+ - Dashboard and config UI now use relative URLs — works behind any URL prefix (e.g. `/airflow/watchdog/`)
78
+ - DAG links in dashboard use `base_url` from Airflow config instead of hardcoded `/dags/`
79
+
80
+ ## [0.3.0] - 2026-03-21
81
+
82
+ ### Added
83
+
84
+ - **Schedule anomaly detector** — flags tasks whose start or end time-of-day deviates from historical norms (IQR-based, handles midnight wraparound)
85
+ - **Per-DAG detector enable/disable** — `disable_detectors` (global) and `dag_overrides` (per-DAG) configuration fields
86
+ - **Configuration UI** at `/watchdog/config` — toggle detectors on/off globally or per DAG with a visual grid
87
+ - **MS Teams alerting** via Adaptive Card webhook (`alert_teams_webhook` config)
88
+ - **Discord alerting** via webhook (`alert_discord_webhook` config)
89
+ - `schedule_interval_minutes` config now wired to DAG schedule (read at parse time)
90
+
91
+ ### Fixed
92
+
93
+ - Email alerts now HTML-escape DAG/task IDs to prevent XSS in email clients
94
+ - Config POST endpoint validates detector names against `AlertType` enum
95
+ - Config page dirty-check bug after saving (Save button now re-enables correctly)
96
+ - Consolidated duplicate `_fmt` duration helper into shared `_stats.fmt_duration`
97
+ - Naive datetimes now respect `airflow.settings.TIMEZONE` instead of assuming UTC
98
+
99
+ ## [0.2.0] - 2026-03-21
100
+
101
+ ### Added
102
+
103
+ - Multi-database support — detectors and dashboard now work with PostgreSQL, MySQL, and SQLite
104
+ - Dashboard error indicator — shows a visible error message instead of a blank page on DB failures
105
+ - Light/dark theme support — dashboard follows OS `prefers-color-scheme`
106
+ - Dashboard tests (8 new tests covering data assembly, endpoints, XSS escaping)
107
+ - `py.typed` marker for type checker support
108
+ - `CHANGELOG.md`
109
+
110
+ ### Changed
111
+
112
+ - Renamed DAG from `watchdog_monitor` to `airflow_watchdog_monitor` to avoid collisions
113
+ - Detectors compute statistics in Python instead of PostgreSQL-specific `PERCENTILE_CONT`
114
+ - Dashboard SQL replaced `LEFT JOIN LATERAL`, `EXTRACT(EPOCH FROM)`, `NOW()` with standard SQL
115
+
116
+ ### Fixed
117
+
118
+ - XSS vulnerability in dashboard — replaced incomplete `</` escaping with OWASP-recommended unicode escapes
119
+ - Type safety — replaced `type: ignore` with `cast()` in alerting module
120
+ - Airflow 3 compatibility — DAG `tags` changed from `list` to `set`
121
+
122
+ ## [0.1.0] - 2026-03-20
123
+
124
+ ### Added
125
+
126
+ - Four health detectors: runtime anomaly (IQR), failure spike, missed deadline, stuck task
127
+ - Auto-registered `airflow_watchdog_monitor` DAG with configurable schedule
128
+ - Dark-themed `/watchdog/` dashboard with auto-refresh (FastAPI + Airflow plugin)
129
+ - Alerting via Airflow logs, email, and Slack webhook
130
+ - JSON-based configuration via Airflow Variable `watchdog_config`
131
+ - Support for Python 3.10–3.13 and Apache Airflow 3.0+
@@ -0,0 +1,191 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to the Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by the Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding any notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ Copyright 2025 Qing
180
+
181
+ Licensed under the Apache License, Version 2.0 (the "License");
182
+ you may not use this file except in compliance with the License.
183
+ You may obtain a copy of the License at
184
+
185
+ http://www.apache.org/licenses/LICENSE-2.0
186
+
187
+ Unless required by applicable law or agreed to in writing, software
188
+ distributed under the License is distributed on an "AS IS" BASIS,
189
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190
+ See the License for the specific language governing permissions and
191
+ limitations under the License.