diff --git a/.github/workflows/merge-gate.yml b/.github/workflows/merge-gate.yml index e0d7598f5..a5d8b827e 100644 --- a/.github/workflows/merge-gate.yml +++ b/.github/workflows/merge-gate.yml @@ -120,7 +120,7 @@ jobs: # Keep this in sync with the underlying workflows. # NOTE: 'gate' (this job) MUST NOT appear here -- it would # deadlock waiting for itself. - EXPECTED_CHECKS: ${{ github.event_name == 'merge_group' && 'Build & Test (Linux),APM Self-Check,Build (Linux),Smoke Test (Linux),Integration Tests (Linux),Release Validation (Linux)' || 'Build & Test (Linux),APM Self-Check' }} + EXPECTED_CHECKS: ${{ github.event_name == 'merge_group' && 'Build & Test (Linux),APM Self-Check,NOTICE Drift Check,Build (Linux),Smoke Test (Linux),Integration Tests (Linux),Release Validation (Linux)' || 'Build & Test (Linux),APM Self-Check,NOTICE Drift Check' }} # Poll budget: ci-integration.yml chains Build -> Smoke -> # Integration (timeout 20m) -> Release Validation (timeout 20m). # Theoretical worst case ~50m; observed today ~5m end-to-end. diff --git a/.github/workflows/notice-drift.yml b/.github/workflows/notice-drift.yml new file mode 100644 index 000000000..9ce3f854d --- /dev/null +++ b/.github/workflows/notice-drift.yml @@ -0,0 +1,139 @@ +# NOTICE Drift Check -- guards the third-party attribution file (NOTICE.md) +# against silent drift on every PR and every merge-queue entry. Also runs +# a license-policy gate (dependency-review-action) on PR-time only. +# +# Why this gate exists +# -------------------- +# `NOTICE.md` is a legally significant artifact: it lists every third-party +# OSS component shipped inside the apm-cli wheel, with verbatim license +# texts. Microsoft CELA's "Manual NOTICE Generation" process makes this +# file *normative* -- if it drifts from the actual install graph (someone +# bumps a dep but forgets to regenerate, or an upstream re-licenses) we +# ship inaccurate attribution. The fix is to make the file generated +# rather than authored, and gate every PR on `--check` mode. +# +# Why no SBOM emission here +# ------------------------- +# GitHub's dependency graph already produces an SPDX SBOM for this repo +# on demand (REST API: GET /repos/{owner}/{repo}/dependency-graph/sbom). +# Dependabot owns vulnerability + version updates over the same graph. +# Adding a second SBOM generator here would duplicate that without value. +# If a CycloneDX-format SBOM is later required for compliance reporting, +# add a separate workflow keyed off release events rather than overloading +# this gate. +# +# Why the dual pull_request + merge_group trigger +# ----------------------------------------------- +# Same pattern as ci.yml / merge-gate.yml: the merge-queue ruleset also +# requires this check, so the workflow has to fire against the temp merge +# commit produced by GitHub's queue. Without the merge_group trigger the +# 'NOTICE Drift Check' check-run would never report on the temp branch +# SHA and the queue would stall waiting for it (see the post-mortem on +# PR #899 referenced in merge-gate.yml). +# +# Why minimum permissions +# ----------------------- +# The job only reads source -- it does not need write access to anything. +# `dependency-review-action` consumes the dep-graph data attached to the +# pull_request event payload, which works with `contents: read`. Keeping +# this at the floor minimises blast radius if a malicious dep ever runs +# code during `uv sync`. +# +# Why `uv sync --frozen` +# ---------------------- +# `--frozen` refuses to update uv.lock during install. Two reasons: +# 1. Reproducibility: the license text we read from dist-info MUST +# correspond to the locked versions, not whatever's newest at CI +# time. A non-frozen sync could pull a newer LICENSE silently. +# 2. Tampering signal: if uv.lock would need to be modified, that's a +# sign someone changed pyproject.toml deps without re-locking -- +# the gate should fail loudly so the author runs `uv lock` locally. + +name: NOTICE Drift Check + +on: + pull_request: + branches: [ main ] + merge_group: + branches: [ main ] + types: [ checks_requested ] + +permissions: + contents: read + +# Dedup rapid pushes on the same PR / merge-queue entry. Same shape as +# merge-gate.yml so the cancellation semantics are uniform across the +# repo's required checks. +concurrency: + group: notice-drift-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + notice-drift: + # The job's `name:` is what GitHub displays as the check-run name + # AND what merge-gate.yml polls in EXPECTED_CHECKS. Renaming this + # value MUST be accompanied by an edit to merge-gate.yml's env. + name: NOTICE Drift Check + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + # Pinned to the same Python version as ci.yml. NOTICE.md content + # depends on which dist-info layout the installed wheels use, and + # different interpreters can resolve different conditional deps + # (e.g. tomli is only installed under python_version<'3.11'). + # Locking to 3.12 keeps the rendered output deterministic across + # CI runs and developer machines. + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install dependencies (frozen) + # --extra dev brings in ruamel.yaml that the generator imports. + # --frozen guarantees we resolve to the exact versions that the + # maintainer locked, so the LICENSE text read from dist-info + # matches what NOTICE.md was generated against locally. + run: uv sync --frozen --extra dev + + # Drift check: regenerate NOTICE.md to memory and diff against the + # committed copy. Exit 1 with a unified diff to stderr if they + # differ -- the diff lands in the GitHub Actions log so the PR + # author can see exactly what to regenerate. + - name: Verify NOTICE.md is up to date + run: uv run python scripts/generate-notice.py --check + + # Supply-chain hygiene: surface any newly-introduced dep whose + # license is incompatible with our redistribution model BEFORE + # the PR merges. We deny the strong-copyleft families (GPL/AGPL) + # and SSPL because apm-cli ships as a single binary; pulling in + # a GPL dep would virally license the whole binary. MPL-2.0 is + # explicitly allowed because we already depend transitively on + # MPL-2.0 components (e.g. certifi-style cert bundles) and the + # MPL's file-level copyleft does not affect our combined work. + # + # Skipped under merge_group because the action requires a + # pull_request context (the dep-graph diff is computed from the + # PR base/head refs, which don't exist in the merge-queue temp + # branch view). + # NOTE: dependency-review-action rejects specifying both + # `allow-licenses` and `deny-licenses` ("You cannot specify both"), + # so we use the deny-list form. That's the safer choice anyway -- + # an allow-list would fail closed on every novel-but-permissive + # SPDX identifier the upstream metadata throws at us (BlueOak, + # 0BSD-variants, dual-licensed `MIT OR Apache-2.0` strings, etc.) + # and produce noisy false positives on otherwise-fine PRs. + - name: License policy check (PR only) + if: github.event_name == 'pull_request' + uses: actions/dependency-review-action@v4 + with: + deny-licenses: GPL-2.0, GPL-3.0, AGPL-3.0, SSPL-1.0 + fail-on-severity: moderate diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..bdacbe96d --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +# Minimal Makefile -- DX shortcut for the NOTICE-file generator. +# Add other targets here as the project grows. + +.PHONY: notice notice-check + +# Regenerate NOTICE.md from pyproject.toml + scripts/notice-metadata.yaml. +# Run this whenever you add / remove / bump a runtime dependency. +notice: + uv run python scripts/generate-notice.py + +# Same check that .github/workflows/notice-drift.yml runs in CI; useful +# for verifying locally before pushing. +notice-check: + uv run python scripts/generate-notice.py --check diff --git a/scripts/generate-notice.py b/scripts/generate-notice.py new file mode 100644 index 000000000..5afaee93a --- /dev/null +++ b/scripts/generate-notice.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +"""Generate / verify the project NOTICE.md. + +WHY this script exists +---------------------- +Microsoft CELA's third-party-notices process requires a per-component +attribution document (`NOTICE.md`) covering every open-source dependency +distributed with the project (the runtime deps of `apm-cli`). Maintaining +that file by hand drifts the moment anyone bumps a version: the version +string, the copyright snippet, and even the *license text* can change +when an upstream re-licenses or re-authors. Drift means we ship stale or +incorrect attribution -- a real legal/compliance risk. + +The generator combines two trusted inputs: + + 1. `pyproject.toml` `[project] dependencies` -- the *direct* runtime + deps. Industry-standard NOTICE scope: transitive deps inherit + attribution via their parents and are typically covered by their + own NOTICE files (or are build-only, like `setuptools`/`wheel`, + which we explicitly exclude in the preamble). + + 2. `scripts/notice-metadata.yaml` -- curated overrides authored by a + human reviewer the first time a dep is added. Holds the SPDX + identifier, upstream URL, copyright snippet, optional notes, and + verbatim "additional attribution" text (e.g. the Apache-2.0 NOTICE + that `requests` ships, or an `AUTHORS` file). PyPI metadata is + unreliable for these fields (some packages mis-declare their + license, others omit homepage, snippets need legal review), so the + YAML is the human-authored source of truth. + +License *text* (the verbatim block) is read at runtime from the +installed `*.dist-info/licenses/LICENSE*` files (PEP 639, Python +3.10+ wheels) -- with fall-backs for older wheel layouts. This keeps +the file synchronised with whatever is actually installed in the uv +lockfile, so a `requests` minor bump that ships an updated LICENSE +will get picked up by `--check` automatically. + +SBOM is intentionally NOT generated by this script. GitHub's dependency +graph already produces an SPDX SBOM for the repo on demand (Settings > +Security > Software bill of materials, or via the REST API at +GET /repos/{owner}/{repo}/dependency-graph/sbom). Adding a second SBOM +generator here would duplicate that surface without adding value. + +Modes +----- + default : regenerate NOTICE.md (overwrite on disk). + --check : regenerate to memory, compare to NOTICE.md on disk, exit 1 + with a unified diff to stderr if they differ. Used by + .github/workflows/notice-drift.yml. + +Exit codes +---------- + 0 file written, or --check passed. + 1 --check mode: drift detected (diff printed to stderr). + 2 any error (missing metadata, dep not installed, malformed YAML). +""" + +from __future__ import annotations + +import argparse +import difflib +import importlib.metadata as ilmd +import re +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path + +# tomllib is stdlib in 3.11+; this script requires Python 3.10+ runtime +# but pyproject.toml parsing only needs to work on the developer / CI +# machine (3.12 per pyproject.toml). On 3.10 we fall back to `tomli`. +try: + import tomllib # type: ignore[import-not-found] +except ModuleNotFoundError: # pragma: no cover -- 3.10 path + import tomli as tomllib # type: ignore[no-redef] + +from ruamel.yaml import YAML + +REPO_ROOT = Path(__file__).resolve().parent.parent +PYPROJECT = REPO_ROOT / "pyproject.toml" +METADATA_YAML = REPO_ROOT / "scripts" / "notice-metadata.yaml" +NOTICE_OUT = REPO_ROOT / "NOTICE.md" + +# Header that prefixes every NOTICE.md. The explanatory paragraph is in +# the YAML (`_header.preamble`) so legal-review of wording lives next +# to the per-component data, not in code. +_FIXED_TOP = ( + "NOTICES\n" + "\n" + "This repository incorporates material as listed below or described in the code.\n" + "\n" + "---\n" + "\n" +) + + +# --------------------------------------------------------------------------- +# Data shapes +# --------------------------------------------------------------------------- + +@dataclass +class DepSpec: + """A single entry parsed from `[project] dependencies`. + + `raw_specifier` preserves everything after the package name -- including + PEP 508 environment markers (e.g. `; python_version<'3.11'`). We keep + it verbatim because the NOTICE.md "Version requirement" line is meant + to communicate *intent* (what the project asks for), not the resolved + version (which lives in `uv.lock` / GitHub's dependency graph). + """ + + name: str + raw_specifier: str # e.g. ">=8.0.0" or ">=1.2.0; python_version<'3.11'" + + +@dataclass +class ComponentMeta: + """Curated per-component data loaded from notice-metadata.yaml.""" + + name: str # canonical/display name as it appears in NOTICE.md + pyproject_name: str # exact name used in pyproject.toml deps + upstream: str + spdx: str + copyright_snippet: str + notes: str | None = None + additional_attribution: dict | None = None # {"source": str, "text": str} + license_text_override: str | None = None # used when dist-info unavailable + # Optional verbatim override for the "Version requirement" cell. Use when + # the pyproject specifier needs an annotation (e.g. PyYAML is declared as + # `pyyaml` lower-case but distributed as `PyYAML`, and we want that + # surfaced in the NOTICE alongside the version). + version_requirement_override: str | None = None + + +# --------------------------------------------------------------------------- +# Parsing helpers +# --------------------------------------------------------------------------- + +# PEP 508 dep spec splitter: name is up to the first non-name char. We only +# need name + everything-after; pip / uv have already validated specifiers. +_NAME_BOUNDARY = re.compile(r"^([A-Za-z0-9_.\-]+)(.*)$") + + +def parse_dependencies(pyproject_path: Path) -> list[DepSpec]: + data = tomllib.loads(pyproject_path.read_text(encoding="utf-8")) + raw_deps = data.get("project", {}).get("dependencies", []) + out: list[DepSpec] = [] + for entry in raw_deps: + m = _NAME_BOUNDARY.match(entry.strip()) + if not m: + raise SystemExit(f"error: cannot parse dependency '{entry}'") + out.append(DepSpec(name=m.group(1), raw_specifier=m.group(2).strip())) + return out + + +def load_metadata(yaml_path: Path) -> tuple[str, dict[str, ComponentMeta]]: + """Return (preamble_text, name->ComponentMeta map keyed by pyproject_name).""" + yaml = YAML(typ="rt") + data = yaml.load(yaml_path.read_text(encoding="utf-8")) + preamble = str(data["_header"]["preamble"]).rstrip("\n") + out: dict[str, ComponentMeta] = {} + for raw in data["components"]: + out[str(raw["pyproject_name"])] = ComponentMeta( + name=str(raw["name"]), + pyproject_name=str(raw["pyproject_name"]), + upstream=str(raw["upstream"]), + spdx=str(raw["spdx"]), + copyright_snippet=str(raw["copyright_snippet"]), + notes=str(raw["notes"]) if raw.get("notes") is not None else None, + additional_attribution=( + { + "source": str(raw["additional_attribution"]["source"]), + "text": str(raw["additional_attribution"]["text"]).rstrip("\n"), + } + if raw.get("additional_attribution") + else None + ), + license_text_override=( + str(raw["license_text"]).rstrip("\n") + if raw.get("license_text") is not None + else None + ), + version_requirement_override=( + str(raw["version_requirement"]) + if raw.get("version_requirement") is not None + else None + ), + ) + return preamble, out + + +# --------------------------------------------------------------------------- +# License-text discovery +# --------------------------------------------------------------------------- + +# Match the *filename* (not full path) of a plausible license file inside a +# dist-info. Order matters at the call site -- we prefer the PEP 639 +# `licenses/` subdirectory layout because that's what modern (>=3.10) wheels +# emit; we then accept the legacy top-level layout, and finally the British +# spelling (`LICENCE`) used by tomli among others. +_LICENSE_FILENAME = re.compile( + r"^(LICENSE|LICENCE|COPYING|NOTICE)(\.[A-Za-z0-9]+)?$" +) + + +def _is_license_file(rel_path: str) -> bool: + fname = rel_path.rsplit("/", 1)[-1] + if not _LICENSE_FILENAME.match(fname): + return False + # Don't pick up upstream NOTICE files as the *license* (those go in + # `additional_attribution`); we only want the actual license body here. + if fname.upper().startswith("NOTICE"): + return False + # Skip AUTHORS-style files even if PEP 639 stuffed them under licenses/ + # (gitpython does this). + if "AUTHOR" in fname.upper(): + return False + return True + + +def read_installed_license(pyproject_name: str) -> str | None: + """Return the verbatim license text from the package's dist-info, or None. + + Tries PEP 639 layout first (`/licenses/LICENSE*`), then the + legacy `/LICENSE*`, and finally `/LICENCE*`. If + the package is not installed or has no license file in the dist-info, + returns None and the caller decides what to do (typically: fall back to + the YAML override, or fail). + """ + try: + dist = ilmd.distribution(pyproject_name) + except ilmd.PackageNotFoundError: + return None + files = dist.files or [] + # Bucket by layout preference: PEP 639 wins, then legacy, then anything. + pep639: list = [] + legacy: list = [] + for f in files: + s = str(f) + if not _is_license_file(s): + continue + if "/licenses/" in s.replace("\\", "/"): + pep639.append(f) + else: + legacy.append(f) + chosen = (pep639 or legacy) + if not chosen: + return None + # Prefer LICENSE over COPYING when both are present. + chosen.sort(key=lambda f: (0 if "LICEN" in str(f).upper() else 1, str(f))) + raw = chosen[0].read_text(encoding="utf-8") + # Some upstreams (requests, watchdog ship Apache-2.0) prefix their LICENSE + # file with a leading blank line. The CELA template doesn't want padding + # *inside* the fenced code block, so we strip leading/trailing newlines. + return raw.strip("\n") + + +# --------------------------------------------------------------------------- +# Renderer +# --------------------------------------------------------------------------- + +def render_component(dep: DepSpec, meta: ComponentMeta, license_text: str) -> str: + parts: list[str] = [] + parts.append(f"## Component. {meta.name}\n\n") + version_req = meta.version_requirement_override or dep.raw_specifier + parts.append(f"- Version requirement: `{version_req}`\n") + parts.append(f"- Upstream: {meta.upstream}\n") + parts.append(f"- SPDX: `{meta.spdx}`\n") + if meta.notes: + parts.append(f"- Notes: {meta.notes}\n") + parts.append("\n### Open Source License/Copyright Notice.\n\n") + parts.append(f"_{meta.copyright_snippet}_\n\n") + parts.append("```\n") + parts.append(license_text) + parts.append("\n```\n") + if meta.additional_attribution: + src = meta.additional_attribution["source"] + txt = meta.additional_attribution["text"] + parts.append(f"\n### Additional Attribution. ({src}, verbatim)\n\n") + parts.append("```\n") + parts.append(txt) + parts.append("\n```\n") + parts.append("\n---\n") + return "".join(parts) + + +def _normalize(name: str) -> str: + """PEP 503 name normalization: lowercase, [-_.] -> '-'.""" + return re.sub(r"[-_.]+", "-", name).lower() + + +def render_notice(deps: list[DepSpec], preamble: str, + metas: dict[str, ComponentMeta]) -> str: + norm_metas = {_normalize(k): v for k, v in metas.items()} + out: list[str] = [_FIXED_TOP, preamble, "\n\n---\n\n"] + for dep in deps: + meta = norm_metas.get(_normalize(dep.name)) + if meta is None: + raise SystemExit( + f"error: dependency '{dep.name}' has no entry in " + f"{METADATA_YAML.relative_to(REPO_ROOT)}. Add a curated " + f"metadata block (upstream, SPDX, copyright_snippet) and " + f"re-run." + ) + license_text = read_installed_license(meta.pyproject_name) + if license_text is None: + license_text = meta.license_text_override + if license_text is None: + raise SystemExit( + f"error: cannot locate LICENSE text for '{dep.name}'. " + f"Either install the package into the active environment " + f"(uv sync) so it can be read from dist-info, or add a " + f"`license_text:` literal block override to its entry in " + f"{METADATA_YAML.relative_to(REPO_ROOT)}." + ) + out.append(render_component(dep, meta, license_text)) + # Trailing blank line between components (and after the last one) + # to match the CELA template -- the file ends `---\n\n`. + out.append("\n") + return "".join(out) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser( + description="Regenerate or validate NOTICE.md for apm-cli.", + ) + p.add_argument( + "--check", + action="store_true", + help="Do not write; exit 1 with unified diff if NOTICE.md is stale.", + ) + args = p.parse_args(argv) + + try: + deps = parse_dependencies(PYPROJECT) + preamble, metas = load_metadata(METADATA_YAML) + rendered = render_notice(deps, preamble, metas) + except SystemExit: + raise + except Exception as e: # pragma: no cover -- defensive + print(f"error: {e}", file=sys.stderr) + return 2 + + if args.check: + on_disk = NOTICE_OUT.read_text(encoding="utf-8") if NOTICE_OUT.exists() else "" + if on_disk == rendered: + return 0 + diff = difflib.unified_diff( + on_disk.splitlines(keepends=True), + rendered.splitlines(keepends=True), + fromfile=str(NOTICE_OUT.relative_to(REPO_ROOT)) + " (on disk)", + tofile=str(NOTICE_OUT.relative_to(REPO_ROOT)) + " (regenerated)", + ) + sys.stderr.write( + "NOTICE.md is out of date with pyproject.toml + notice-metadata.yaml.\n" + "Run `make notice` (or `python scripts/generate-notice.py`) and commit.\n\n" + ) + sys.stderr.writelines(diff) + return 1 + + NOTICE_OUT.write_text(rendered, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/notice-metadata.yaml b/scripts/notice-metadata.yaml new file mode 100644 index 000000000..9b16ee4a8 --- /dev/null +++ b/scripts/notice-metadata.yaml @@ -0,0 +1,257 @@ +_header: + preamble: | + This file is generated and maintained per the Microsoft CELA "Manual NOTICE + Generation" process. It enumerates third-party open-source components that ship + with this project (the runtime dependencies of the `apm` Python package). Build- + time and contributor-only tooling (test runners, linters, type-checkers, the + PyInstaller build backend, and the `setuptools` / `wheel` build system) are not + distributed and are intentionally omitted, as is install-time user state created + by the CLI under `apm_modules/`. No third-party source is vendored into this + repository. + + The `apm` source code itself is licensed under the MIT License; see `LICENSE`. +components: + - name: click + pyproject_name: click + upstream: https://github.com/pallets/click + spdx: BSD-3-Clause + copyright_snippet: Copyright 2014 Pallets + - name: colorama + pyproject_name: colorama + upstream: https://github.com/tartley/colorama + spdx: BSD-3-Clause + copyright_snippet: Copyright (c) 2010 Jonathan Hartley + - name: PyYAML + pyproject_name: pyyaml + upstream: https://github.com/yaml/pyyaml + spdx: MIT + copyright_snippet: Copyright (c) 2017-2021 Ingy doet Net + version_requirement: '>=6.0.0 (declared as `pyyaml`)' + - name: requests + pyproject_name: requests + upstream: https://github.com/psf/requests + spdx: Apache-2.0 + copyright_snippet: Copyright 2019 Kenneth Reitz (per upstream NOTICE file) + notes: Apache-2.0 requires forwarding the upstream NOTICE file verbatim. + additional_attribution: + source: upstream NOTICE file + text: | + Requests + Copyright 2019 Kenneth Reitz + - name: python-frontmatter + pyproject_name: python-frontmatter + upstream: https://github.com/eyeseast/python-frontmatter + spdx: MIT + copyright_snippet: Copyright (c) 2021 Chris Amico + - name: llm + pyproject_name: llm + upstream: https://github.com/simonw/llm + spdx: Apache-2.0 + copyright_snippet: Copyright Simon Willison (per PyPI author metadata; LICENSE file is the unmodified Apache 2.0 boilerplate with no embedded copyright statement) + notes: No NOTICE or AUTHORS file present upstream; nothing additional to forward. + - name: llm-github-models + pyproject_name: llm-github-models + upstream: https://github.com/tonybaloney/llm-github-models + spdx: Apache-2.0 + copyright_snippet: 'Copyright (c) 2025 Anthony Shaw (LICENSE header) -- note: file contents are MIT-style header followed by Apache-2.0 reference; see Open Issues' + notes: Upstream LICENSE file declares MIT license text but PyPI metadata + classifiers say Apache-2.0. See Open Issues. + - name: tomli + pyproject_name: tomli + upstream: https://github.com/hukkin/tomli + spdx: MIT + copyright_snippet: Copyright (c) 2021 Taneli Hukkinen + notes: 'Conditional dependency: only installed on Python < 3.11. Still ships in the sdist/wheel install graph.' + license_text: | + MIT License + + Copyright (c) 2021 Taneli Hukkinen + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + - name: toml + pyproject_name: toml + upstream: https://github.com/uiri/toml + spdx: MIT + copyright_snippet: Copyright 2013-2019 William Pearson + - name: rich + pyproject_name: rich + upstream: https://github.com/Textualize/rich + spdx: MIT + copyright_snippet: Copyright (c) 2020 Will McGugan + - name: rich-click + pyproject_name: rich-click + upstream: https://github.com/ewels/rich-click + spdx: MIT + copyright_snippet: Copyright (c) 2022 Phil Ewels + - name: watchdog + pyproject_name: watchdog + upstream: https://github.com/gorakhargosh/watchdog + spdx: Apache-2.0 + copyright_snippet: Copyright Yesudeep Mangalapilly, Mickael Schoentgen, and watchdog contributors (per upstream AUTHORS file; LICENSE file is the unmodified Apache 2.0 boilerplate) + notes: No NOTICE file upstream; AUTHORS reproduced for attribution. + additional_attribution: + source: upstream AUTHORS file + text: | + Original Project Lead: + ---------------------- + Yesudeep Mangalapilly + + Current Project Lead: + --------------------- + Mickaël Schoentgen + + Contributors in alphabetical order: + ----------------------------------- + Adrian Tejn Kern + Andrew Schaaf + Danilo de Jesus da Silva Bellini + David LaPalomento + dvogel + Filip Noetzel + Gary van der Merwe + gfxmonk + Gora Khargosh + Hannu Valtonen + Jesse Printz + Kurt McKee + Léa Klein + Luke McCarthy + Lukáš Lalinský + Malthe Borch + Martin Kreichgauer + Martin Kreichgauer + Mike Lundy + Nicholas Hairs + Raymond Hettinger + Roman Ovchinnikov + Rotem Yaari + Ryan Kelly + Senko Rasic + Senko Rašić + Shane Hathaway + Simon Pantzare + Simon Pantzare + Steven Samuel Cole + Stéphane Klein + Thomas Guest + Thomas Heller + Tim Cuthbertson + Todd Whiteman + Will McGugan + Yesudeep Mangalapilly + Yesudeep Mangalapilly + + We would like to thank these individuals for ideas: + --------------------------------------------------- + Tim Golden + Sebastien Martini + + Initially we used the flask theme for the documentation which was written by + ---------------------------------------------------------------------------- + Armin Ronacher + + + Watchdog also includes open source libraries or adapted code + from the following projects: + + - MacFSEvents - https://github.com/malthe/macfsevents + - watch_directory.py - http://timgolden.me.uk/python/downloads/watch_directory.py + - pyinotify - https://github.com/seb-m/pyinotify + - fsmonitor - https://github.com/shaurz/fsmonitor + - echo - http://wordaligned.org/articles/echo + - Lukáš Lalinský's ordered set queue implementation: + https://stackoverflow.com/questions/1581895/how-check-if-a-task-is-already-in-python-queue + - Armin Ronacher's flask-sphinx-themes for the documentation: + https://github.com/mitsuhiko/flask-sphinx-themes + - pyfilesystem - https://github.com/PyFilesystem/pyfilesystem + - get_FILE_NOTIFY_INFORMATION - http://blog.gmane.org/gmane.comp.python.ctypes/month=20070901 + - name: GitPython + pyproject_name: gitpython + upstream: https://github.com/gitpython-developers/GitPython + spdx: BSD-3-Clause + copyright_snippet: Copyright (C) 2008, 2009 Michael Trier and contributors + additional_attribution: + source: upstream AUTHORS file + text: | + GitPython was originally written by Michael Trier. + GitPython 0.2 was partially (re)written by Sebastian Thiel, based on 0.1.6 and git-dulwich. + + Contributors are: + + -Michael Trier + -Alan Briolat + -Florian Apolloner + -David Aguilar + -Jelmer Vernooij + -Steve Frécinaux + -Kai Lautaportti + -Paul Sowden + -Sebastian Thiel + -Jonathan Chu + -Vincent Driessen + -Phil Elson + -Bernard `Guyzmo` Pratz + -Timothy B. Hartman + -Konstantin Popov + -Peter Jones + -Anson Mansfield + -Ken Odegard + -Alexis Horgix Chotard + -Piotr Babij + -Mikuláš Poul + -Charles Bouchard-Légaré + -Yaroslav Halchenko + -Tim Swast + -William Luc Ritchie + -David Host + -A. Jesse Jiryu Davis + -Steven Whitman + -Stefan Stancu + -César Izurieta + -Arthur Milchior + -Anil Khatri + -JJ Graham + -Ben Thayer + -Dries Kennes + -Pratik Anurag + -Harmon + -Liam Beguin + -Ram Rachum + -Alba Mendez + -Robert Westman + -Hugo van Kemenade + -Hiroki Tokunaga + -Julien Mauroy + -Patrick Gerard + -Luke Twist + -Joseph Hale + -Santos Gallegos + -Wenhan Zhu + -Eliah Kagan + -Ethan Lin + -Jonas Scharpf + -Gordon Marx + -Enji Cooper + + Portions derived from other open source works and are clearly marked. + - name: ruamel.yaml + pyproject_name: ruamel.yaml + upstream: 'https://sourceforge.net/p/ruamel-yaml/code/ (mirror: https://pypi.org/project/ruamel.yaml/)' + spdx: MIT + copyright_snippet: Copyright (c) 2014-2026 Anthon van der Neut, Ruamel bvba + notes: Canonical source is SourceForge (Mercurial). LICENSE retrieved from the official PyPI sdist tarball ruamel_yaml-0.19.1.tar.gz.