diff --git a/Makefile b/Makefile index 2338059..13bb031 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,11 @@ # Copyright 2025 Itential Inc. All Rights Reserved # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later .DEFAULT_GOAL := help -.PHONY: test coverage clean lint format ruff-fix security tox \ +.PHONY: test coverage clean lint format ruff-fix security license license-fix tox \ tox-py310 tox-py311 tox-py312 tox-py313 tox-coverage tox-lint \ tox-format tox-security tox-premerge tox-list @@ -17,6 +18,8 @@ help: @echo " clean - Cleans the development environment" @echo " coverage - Run test coverage report" @echo " format - Format source files with ruff" + @echo " license - Check all Python files for proper license headers" + @echo " license-fix - Automatically add missing license headers to Python files" @echo " lint - Run analysis on source files" @echo " premerge - Run the premerge tests locally" @echo " ruff-fix - Run ruff with --fix to auto-fix issues" @@ -54,12 +57,22 @@ lint: uv run ruff check src/ipsdk uv run ruff check tests -# The security target invokes bandit to run security analysis on the +# The security target invokes bandit to run security analysis on the # source code. It scans for common security vulnerabilities. security: uv run bandit -r src/ipsdk --configfile pyproject.toml -# The clean target will remove build and dev artififacts that are not +# The license target verifies that all Python files contain the +# proper license header. This target is invoked in the premerge pipeline. +license: + uv run python scripts/check_license_headers.py + +# The license-fix target automatically adds missing license headers to +# all Python files in the project. +license-fix: + uv run python scripts/check_license_headers.py --fix + +# The clean target will remove build and dev artififacts that are not # part of the application and get created by other targets. clean: @rm -rf .pytest_cache coverage.* htmlcov dist build *.egg-info @@ -77,7 +90,7 @@ ruff-fix: # The premerge target will run the permerge tests locally. This is # the same target that is invoked in the permerge pipeline. -premerge: clean lint security test +premerge: clean lint security license test # The tox target will run tests across all supported Python versions # (3.10, 3.11, 3.12, 3.13) using tox with uv integration. diff --git a/scripts/check_license_headers.py b/scripts/check_license_headers.py new file mode 100755 index 0000000..6910b05 --- /dev/null +++ b/scripts/check_license_headers.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Check that all Python files have proper license headers. + +This script verifies that all .py files in src/ipsdk and tests directories +contain the required copyright, license, and SPDX identifier headers. + +Required headers: + # Copyright (c) 2025 Itential, Inc + # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + # SPDX-License-Identifier: GPL-3.0-or-later + +Usage: + python scripts/check_license_headers.py # Check only + python scripts/check_license_headers.py --fix # Fix missing headers + +Exit codes: + 0 - All files have proper headers (or were fixed) + 1 - One or more files are missing proper headers (check mode only) +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + + +REQUIRED_HEADERS = [ + "# Copyright (c) 2025 Itential, Inc", + "# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)", + "# SPDX-License-Identifier: GPL-3.0-or-later", +] + + +def check_file_header(file_path: Path) -> bool: + """Check if a file has the required license headers. + + Args: + file_path: Path to the Python file to check + + Returns: + True if the file has proper headers, False otherwise + """ + try: + with open(file_path, encoding="utf-8") as f: + lines = [line.rstrip() for line in f.readlines()[:10]] + except Exception as e: + print(f"Error reading {file_path}: {e}") + return False + + # Check if all required headers are present in the first few lines + for required_header in REQUIRED_HEADERS: + if required_header not in lines: + return False + + return True + + +def fix_file_header(file_path: Path) -> bool: + """Add the required license headers to a file. + + This function intelligently handles files that may already have partial headers + by removing any existing header lines and replacing them with the complete set. + + Args: + file_path: Path to the Python file to fix + + Returns: + True if the file was fixed successfully, False otherwise + """ + try: + with open(file_path, encoding="utf-8") as f: + content = f.read() + except Exception as e: + print(f"Error reading {file_path}: {e}") + return False + + lines = content.splitlines(keepends=True) + + # Check if file starts with shebang + shebang_lines = [] + start_position = 0 + if lines and lines[0].startswith("#!"): + shebang_lines = [lines[0]] + start_position = 1 + + # Remove any existing partial headers + # Look for lines that match any part of our required headers + content_start = start_position + for i in range(start_position, min(start_position + 10, len(lines))): + line = lines[i].rstrip() + # Check if this line matches any of our header patterns + is_header_line = False + for header in REQUIRED_HEADERS: + if line == header or line.startswith("# Copyright") or \ + line.startswith("# GNU General Public License") or \ + line.startswith("# SPDX-License-Identifier"): + is_header_line = True + break + + # Stop when we hit a non-header line (docstring, import, etc.) + if not is_header_line and line and not line.startswith("#"): + content_start = i + break + elif not is_header_line and not line: + # Empty line after headers + content_start = i + break + elif is_header_line: + content_start = i + 1 + + # Build the header to insert + header_lines = [f"{header}\n" for header in REQUIRED_HEADERS] + header_lines.append("\n") # Add blank line after headers + + # Build the new file content + new_lines = shebang_lines + header_lines + lines[content_start:] + + try: + with open(file_path, "w", encoding="utf-8") as f: + f.writelines(new_lines) + return True + except Exception as e: + print(f"Error writing {file_path}: {e}") + return False + + +def find_python_files(root_dir: Path) -> list[Path]: + """Find all Python files in a directory recursively. + + Args: + root_dir: Root directory to search + + Returns: + List of Path objects for all .py files found + """ + return sorted(root_dir.rglob("*.py")) + + +def main() -> int: + """Main function to check license headers in all Python files. + + Returns: + Exit code: 0 if all files pass, 1 if any file fails + """ + parser = argparse.ArgumentParser( + description="Check or fix license headers in Python files" + ) + parser.add_argument( + "--fix", + action="store_true", + help="Automatically add missing license headers", + ) + args = parser.parse_args() + + repo_root = Path(__file__).parent.parent + src_dir = repo_root / "src" / "ipsdk" + tests_dir = repo_root / "tests" + + # Find all Python files + python_files: list[Path] = [] + if src_dir.exists(): + python_files.extend(find_python_files(src_dir)) + if tests_dir.exists(): + python_files.extend(find_python_files(tests_dir)) + + if not python_files: + print("No Python files found to check") + return 0 + + # Check each file + files_without_headers: list[Path] = [] + for file_path in python_files: + if not check_file_header(file_path): + files_without_headers.append(file_path) + + # Report results + total_files = len(python_files) + files_with_headers = total_files - len(files_without_headers) + + print(f"Checked {total_files} Python files") + print(f" ✓ {files_with_headers} files have proper headers") + + if files_without_headers: + print(f" ✗ {len(files_without_headers)} files are missing proper headers:") + for file_path in files_without_headers: + rel_path = file_path.relative_to(repo_root) + print(f" - {rel_path}") + + if args.fix: + print() + print("Fixing files...") + fixed_files: list[Path] = [] + failed_files: list[Path] = [] + + for file_path in files_without_headers: + if fix_file_header(file_path): + fixed_files.append(file_path) + else: + failed_files.append(file_path) + + print(f" ✓ Fixed {len(fixed_files)} files") + if failed_files: + print(f" ✗ Failed to fix {len(failed_files)} files:") + for file_path in failed_files: + rel_path = file_path.relative_to(repo_root) + print(f" - {rel_path}") + return 1 + + print() + print("All files now have proper license headers!") + return 0 + else: + print() + print("Required headers:") + for header in REQUIRED_HEADERS: + print(f" {header}") + print() + print("Run with --fix to automatically add missing headers") + return 1 + + print() + print("All files have proper license headers!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/ipsdk/__init__.py b/src/ipsdk/__init__.py index c4873b7..8345277 100644 --- a/src/ipsdk/__init__.py +++ b/src/ipsdk/__init__.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + r"""Itential Python SDK for Itential Platform and Automation Gateway. diff --git a/src/ipsdk/connection.py b/src/ipsdk/connection.py index 80b715a..c71dcdb 100644 --- a/src/ipsdk/connection.py +++ b/src/ipsdk/connection.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import annotations diff --git a/src/ipsdk/exceptions.py b/src/ipsdk/exceptions.py index 6ab0bdd..5f07e20 100644 --- a/src/ipsdk/exceptions.py +++ b/src/ipsdk/exceptions.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import annotations diff --git a/src/ipsdk/gateway.py b/src/ipsdk/gateway.py index 0f29981..77d9f00 100644 --- a/src/ipsdk/gateway.py +++ b/src/ipsdk/gateway.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import annotations diff --git a/src/ipsdk/heuristics.py b/src/ipsdk/heuristics.py index 7c3d9e2..50ee546 100644 --- a/src/ipsdk/heuristics.py +++ b/src/ipsdk/heuristics.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import annotations diff --git a/src/ipsdk/http.py b/src/ipsdk/http.py index 429eee7..140af1a 100644 --- a/src/ipsdk/http.py +++ b/src/ipsdk/http.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import annotations diff --git a/src/ipsdk/jsonutils.py b/src/ipsdk/jsonutils.py index cfd5619..40c869b 100644 --- a/src/ipsdk/jsonutils.py +++ b/src/ipsdk/jsonutils.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + """JSON serialization and deserialization utilities for the Itential Python SDK. diff --git a/src/ipsdk/logging.py b/src/ipsdk/logging.py index ed5280d..4e4c488 100644 --- a/src/ipsdk/logging.py +++ b/src/ipsdk/logging.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import annotations diff --git a/src/ipsdk/metadata.py b/src/ipsdk/metadata.py index bdf3ed3..9a06a62 100644 --- a/src/ipsdk/metadata.py +++ b/src/ipsdk/metadata.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + """Package metadata and version information for the Itential Python SDK. diff --git a/src/ipsdk/platform.py b/src/ipsdk/platform.py index 655cfac..a02f0bd 100644 --- a/src/ipsdk/platform.py +++ b/src/ipsdk/platform.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import annotations diff --git a/tests/__init__.py b/tests/__init__.py index ca60f97..b82765c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,2 +1,4 @@ -# Copyright 2025 Itential Inc. All Rights Reserved +# Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + diff --git a/tests/test_connection.py b/tests/test_connection.py index 1388264..513545e 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + import json import time diff --git a/tests/test_enums.py b/tests/test_enums.py index 779f165..a3a93ec 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from enum import Enum diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 3b10c5c..c6d7c1c 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + """ Test suite for the refactored exception module. diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 6132bfc..def1828 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from unittest.mock import AsyncMock from unittest.mock import Mock diff --git a/tests/test_heuristics.py b/tests/test_heuristics.py index e563ea0..7d7187d 100644 --- a/tests/test_heuristics.py +++ b/tests/test_heuristics.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + import re diff --git a/tests/test_init.py b/tests/test_init.py index a211783..019f0dd 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + """Tests for the ipsdk.__init__ module.""" diff --git a/tests/test_jsonutils.py b/tests/test_jsonutils.py index dbdc494..1e4a9ae 100644 --- a/tests/test_jsonutils.py +++ b/tests/test_jsonutils.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + import datetime import decimal diff --git a/tests/test_logging.py b/tests/test_logging.py index 0630bf2..3b8fde3 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + import asyncio import contextlib diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 573eb30..c933343 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + import ipsdk.metadata diff --git a/tests/test_platform.py b/tests/test_platform.py index cf39ab1..2da3837 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from unittest.mock import AsyncMock from unittest.mock import Mock