From db63897c9fa5622f1c0aeba93e93ab93831a6cc7 Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Sun, 14 Jun 2026 21:15:12 -0500 Subject: [PATCH] chore: add SPDX license headers and NOTICE file Stamp every Python source file with SPDX headers (Apache-2.0, AG Technology Group LLC) and add an attribution-only NOTICE. Enforcement is CI-only (no local hook): the lint job runs `python scripts/license_header.py --check`, which verifies each tracked .py file carries an SPDX identifier (run --fix to stamp new files). Kept out of pre-commit so anyone cloning the template doesn't inherit a local hook they'd have to strip out. --- .github/workflows/ci.yml | 1 + NOTICE | 4 + alembic/env.py | 3 + .../versions/af85030565fd_initial_schema.py | 3 + .../b3c7a1d9e2f4_add_user_role_column.py | 3 + .../c4e8f2a1b5d7_add_user_name_column.py | 3 + app/__init__.py | 3 + app/analytics.py | 3 + app/auth/__init__.py | 3 + app/auth/backend.py | 3 + app/auth/refresh.py | 3 + app/auth/roles.py | 3 + app/auth/security_logging.py | 3 + app/auth/users.py | 3 + app/config.py | 3 + app/database.py | 3 + app/features.py | 3 + app/logging.py | 3 + app/main.py | 3 + app/models/__init__.py | 3 + app/models/note.py | 3 + app/models/refresh_token.py | 3 + app/models/user.py | 3 + app/routers/__init__.py | 3 + app/routers/admin.py | 3 + app/routers/auth_refresh.py | 3 + app/routers/notes.py | 3 + app/schemas/__init__.py | 3 + app/schemas/note.py | 3 + app/schemas/user.py | 3 + app/telemetry.py | 3 + scripts/license_header.py | 113 ++++++++++++++++++ tests/__init__.py | 3 + tests/conftest.py | 3 + tests/test_app.py | 3 + tests/test_auth.py | 3 + tests/test_notes.py | 3 + tests/test_roles.py | 3 + 38 files changed, 223 insertions(+) create mode 100644 NOTICE create mode 100644 scripts/license_header.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1077d1c..3f55448 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: - run: uv sync - run: uv run ruff check . - run: uv run ruff format --check . + - run: uv run python scripts/license_header.py --check test: runs-on: ubuntu-latest diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..44ab902 --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +AG Tech API Template +Copyright 2026 AG Technology Group LLC + +This product includes software developed by AG Technology Group LLC. diff --git a/alembic/env.py b/alembic/env.py index 0c1f4ba..66c7b2b 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + import asyncio from logging.config import fileConfig diff --git a/alembic/versions/af85030565fd_initial_schema.py b/alembic/versions/af85030565fd_initial_schema.py index 76acd4f..839b4d0 100644 --- a/alembic/versions/af85030565fd_initial_schema.py +++ b/alembic/versions/af85030565fd_initial_schema.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + """initial schema Revision ID: af85030565fd diff --git a/alembic/versions/b3c7a1d9e2f4_add_user_role_column.py b/alembic/versions/b3c7a1d9e2f4_add_user_role_column.py index 6f84312..ecb7d36 100644 --- a/alembic/versions/b3c7a1d9e2f4_add_user_role_column.py +++ b/alembic/versions/b3c7a1d9e2f4_add_user_role_column.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + """add user role column Revision ID: b3c7a1d9e2f4 diff --git a/alembic/versions/c4e8f2a1b5d7_add_user_name_column.py b/alembic/versions/c4e8f2a1b5d7_add_user_name_column.py index 2a2f726..87c51b8 100644 --- a/alembic/versions/c4e8f2a1b5d7_add_user_name_column.py +++ b/alembic/versions/c4e8f2a1b5d7_add_user_name_column.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + """add user name column Revision ID: c4e8f2a1b5d7 diff --git a/app/__init__.py b/app/__init__.py index e69de29..7b0a131 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + diff --git a/app/analytics.py b/app/analytics.py index 410f133..4f7751a 100644 --- a/app/analytics.py +++ b/app/analytics.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + """Analytics event abstraction with a pluggable backend.""" from __future__ import annotations diff --git a/app/auth/__init__.py b/app/auth/__init__.py index 2e2b9f8..37f4511 100644 --- a/app/auth/__init__.py +++ b/app/auth/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from app.auth.roles import UserRole, require_role from app.auth.users import auth_backend, current_active_user, fastapi_users diff --git a/app/auth/backend.py b/app/auth/backend.py index bb9ad21..b385305 100644 --- a/app/auth/backend.py +++ b/app/auth/backend.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from fastapi_users.authentication import AuthenticationBackend, CookieTransport, JWTStrategy from app.config import settings diff --git a/app/auth/refresh.py b/app/auth/refresh.py index 89d22db..05f8f2f 100644 --- a/app/auth/refresh.py +++ b/app/auth/refresh.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + import uuid from datetime import UTC, datetime, timedelta diff --git a/app/auth/roles.py b/app/auth/roles.py index 952d4cf..d734101 100644 --- a/app/auth/roles.py +++ b/app/auth/roles.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from enum import StrEnum from fastapi import Depends, HTTPException, status diff --git a/app/auth/security_logging.py b/app/auth/security_logging.py index 9a1ffa0..0bd6e0f 100644 --- a/app/auth/security_logging.py +++ b/app/auth/security_logging.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from enum import StrEnum import structlog diff --git a/app/auth/users.py b/app/auth/users.py index 5f48b6b..cda0af8 100644 --- a/app/auth/users.py +++ b/app/auth/users.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from collections.abc import AsyncGenerator from uuid import UUID diff --git a/app/config.py b/app/config.py index ceb58e0..13fee46 100644 --- a/app/config.py +++ b/app/config.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from pydantic import model_validator from pydantic_settings import BaseSettings, SettingsConfigDict diff --git a/app/database.py b/app/database.py index df6408a..91792bb 100644 --- a/app/database.py +++ b/app/database.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from collections.abc import AsyncGenerator from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine diff --git a/app/features.py b/app/features.py index d845aa0..99a9ce6 100644 --- a/app/features.py +++ b/app/features.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + """Environment-variable-backed feature flags.""" from __future__ import annotations diff --git a/app/logging.py b/app/logging.py index 0caf239..fd86c00 100644 --- a/app/logging.py +++ b/app/logging.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + """Structured logging configuration using structlog.""" import logging diff --git a/app/main.py b/app/main.py index 4a322ea..bc07cc8 100644 --- a/app/main.py +++ b/app/main.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + import time import uuid diff --git a/app/models/__init__.py b/app/models/__init__.py index f2d4930..bf36c60 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from app.models.note import Note from app.models.refresh_token import RefreshToken from app.models.user import User diff --git a/app/models/note.py b/app/models/note.py index 02811ae..0c81004 100644 --- a/app/models/note.py +++ b/app/models/note.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + import uuid from datetime import datetime diff --git a/app/models/refresh_token.py b/app/models/refresh_token.py index c2abe06..efe8cff 100644 --- a/app/models/refresh_token.py +++ b/app/models/refresh_token.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + import uuid from datetime import datetime diff --git a/app/models/user.py b/app/models/user.py index a02c800..5bc018c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from fastapi_users.db import SQLAlchemyBaseUserTableUUID from sqlalchemy import String from sqlalchemy.orm import Mapped, mapped_column diff --git a/app/routers/__init__.py b/app/routers/__init__.py index 958073c..ba3e6d1 100644 --- a/app/routers/__init__.py +++ b/app/routers/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from app.routers.admin import router as admin_router from app.routers.notes import router as notes_router diff --git a/app/routers/admin.py b/app/routers/admin.py index e3464b4..9ae717f 100644 --- a/app/routers/admin.py +++ b/app/routers/admin.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status diff --git a/app/routers/auth_refresh.py b/app/routers/auth_refresh.py index 6338e8b..4754164 100644 --- a/app/routers/auth_refresh.py +++ b/app/routers/auth_refresh.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from uuid import UUID from fastapi import APIRouter, Cookie, Depends, Response diff --git a/app/routers/notes.py b/app/routers/notes.py index 15b9fb5..d0413b3 100644 --- a/app/routers/notes.py +++ b/app/routers/notes.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index 2168739..0e1bf2a 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from app.schemas.note import NoteCreate, NoteRead, NoteUpdate from app.schemas.user import UserCreate, UserRead, UserUpdate diff --git a/app/schemas/note.py b/app/schemas/note.py index 1524547..4ca5a8b 100644 --- a/app/schemas/note.py +++ b/app/schemas/note.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from datetime import datetime from uuid import UUID diff --git a/app/schemas/user.py b/app/schemas/user.py index 914041a..880ee8e 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from uuid import UUID from fastapi_users import schemas diff --git a/app/telemetry.py b/app/telemetry.py index 477e475..91315f9 100644 --- a/app/telemetry.py +++ b/app/telemetry.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + """OpenTelemetry auto-instrumentation for FastAPI.""" from __future__ import annotations diff --git a/scripts/license_header.py b/scripts/license_header.py new file mode 100644 index 0000000..1a2d7f4 --- /dev/null +++ b/scripts/license_header.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Stamp or verify SPDX license headers on Python sources. + +Usage: + python scripts/license_header.py --check verify every tracked .py file (CI) + python scripts/license_header.py --fix add the header where missing + +Default mode is --check. Re-running is safe: a file that already carries an +SPDX identifier in its first lines is left untouched (idempotent), so the CI +check and a manual --fix can never disagree. +""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +HEADER = ( + "# SPDX-FileCopyrightText: 2026 AG Technology Group LLC\n" + "# SPDX-License-Identifier: Apache-2.0\n" +) + +GENERATED_BANNER = re.compile(r"@generated|do not edit|automatically generated", re.IGNORECASE) +ENCODING = re.compile(r"coding[:=]\s*[-\w.]+") + + +def tracked_py_files() -> list[str]: + result = subprocess.run( + ["git", "ls-files", "*.py"], + capture_output=True, + text=True, + check=True, + ) + return [line for line in result.stdout.splitlines() if line] + + +def classify(text: str) -> str: + """Return "stamped", "skip", or "needs".""" + lines = text.splitlines() + if GENERATED_BANNER.search("\n".join(lines[:15])): + return "skip" + if "SPDX-License-Identifier" in "\n".join(lines[:10]): + return "stamped" + return "needs" + + +def apply_header(text: str) -> str: + """Insert the header below a shebang/encoding line, above everything else.""" + lines = text.splitlines(keepends=True) + prefix: list[str] = [] + if lines and lines[0].startswith("#!"): + prefix.append(lines.pop(0)) + if lines and lines[0].lstrip().startswith("#") and ENCODING.search(lines[0]): + prefix.append(lines.pop(0)) + while lines and lines[0].strip() == "": + lines.pop(0) + body = "".join(lines) + separator = "\n" if body else "" + return "".join(prefix) + HEADER + separator + body + + +def main() -> int: + args = sys.argv[1:] + fix = "--fix" in args + explicit = [a for a in args if not a.startswith("--")] + files = explicit or tracked_py_files() + + stamped: list[str] = [] + already: list[str] = [] + skipped: list[str] = [] + missing: list[str] = [] + + for name in files: + path = Path(name) + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + continue + status = classify(text) + if status == "skip": + skipped.append(name) + elif status == "stamped": + already.append(name) + elif fix: + path.write_text(apply_header(text), encoding="utf-8") + stamped.append(name) + else: + missing.append(name) + + if fix: + print( + f"license-header: stamped {len(stamped)}, already {len(already)}, skipped {len(skipped)}" + ) + for name in stamped: + print(f" + {name}") + elif missing: + print(f"license-header: {len(missing)} file(s) missing an SPDX header:", file=sys.stderr) + for name in missing: + print(f" - {name}", file=sys.stderr) + print('Run "uv run python scripts/license_header.py --fix" to add them.', file=sys.stderr) + return 1 + else: + print(f"license-header: OK — {len(already)} stamped, {len(skipped)} skipped") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..7b0a131 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + diff --git a/tests/conftest.py b/tests/conftest.py index 43be725..e51ee5c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from collections.abc import AsyncGenerator from uuid import uuid4 diff --git a/tests/test_app.py b/tests/test_app.py index 00468f4..4615e02 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + """App-level / infrastructure route behaviour: security.txt and global rate limiting.""" from httpx import AsyncClient diff --git a/tests/test_auth.py b/tests/test_auth.py index b32408b..54b65fb 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + import importlib import os from datetime import UTC, datetime, timedelta diff --git a/tests/test_notes.py b/tests/test_notes.py index c51ea98..632784c 100644 --- a/tests/test_notes.py +++ b/tests/test_notes.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from uuid import uuid4 from httpx import AsyncClient diff --git a/tests/test_roles.py b/tests/test_roles.py index e166fc3..6f595ca 100644 --- a/tests/test_roles.py +++ b/tests/test_roles.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 AG Technology Group LLC +# SPDX-License-Identifier: Apache-2.0 + from uuid import uuid4 from httpx import AsyncClient