diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9889895..ed7ef0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: '^{{cookiecutter\.project_name}}/.*' repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: c59bba8fb259db0fec2bbb77ad8ba51ea7341b56 # frozen: v0.15.20 + rev: 2eeb5678de71a00c0902cbda7105d328432f72cb # frozen: v0.16.8 hooks: - id: ruff-check types_or: [ python, pyi, jupyter ] @@ -24,7 +24,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/lycheeverse/lychee - rev: 3d6c1b6bb8d648d6e2949d7e5bd0c9e20d8b41dd # frozen: nightly + rev: 4e065481e8571d0b270c5f4e2332b74cc8342758 # frozen: nightly hooks: - id: lychee-docker # Keep all other configs in the config file @@ -40,7 +40,7 @@ repos: - id: shellcheck args: [ -x, --source-path=SCRIPTDIR ] - repo: https://github.com/trufflesecurity/trufflehog - rev: 00155c9dc586f34d189adc83d3ac2698c2ec551f # frozen: v3.95.8 + rev: f714bf454f350590f4a24c3ddb1aef02c35bf5b6 # frozen: v3.97.5 hooks: - id: trufflehog # Resolve the repo root via git-common-dir so this works in both normal repos and worktrees @@ -53,7 +53,7 @@ repos: hooks: - id: openapi-spec-validator - repo: https://github.com/streetsidesoftware/cspell-cli - rev: ea11f9efc0bec520073405bc30552da887ba71bc # frozen: v10.0.1 + rev: 058f44e47719e80d8f1e597a235310b60e937860 # frozen: v10.2.0 hooks: - id: cspell types: [markdown] diff --git a/{{cookiecutter.project_name}}/.pre-commit-config.yaml b/{{cookiecutter.project_name}}/.pre-commit-config.yaml index d3864bc..36529af 100644 --- a/{{cookiecutter.project_name}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_name}}/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: '^{{ cookiecutter.project_name }}/.*' repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: c59bba8fb259db0fec2bbb77ad8ba51ea7341b56 # frozen: v0.15.20 + rev: 2eeb5678de71a00c0902cbda7105d328432f72cb # frozen: v0.16.8 hooks: - id: ruff-check types_or: [ python, pyi, jupyter ] @@ -24,7 +24,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/lycheeverse/lychee - rev: 3d6c1b6bb8d648d6e2949d7e5bd0c9e20d8b41dd # frozen: nightly + rev: 4e065481e8571d0b270c5f4e2332b74cc8342758 # frozen: nightly hooks: - id: lychee-docker # Keep all other configs in the config file @@ -40,7 +40,7 @@ repos: - id: shellcheck args: [ -x, --source-path=SCRIPTDIR ] - repo: https://github.com/trufflesecurity/trufflehog - rev: 00155c9dc586f34d189adc83d3ac2698c2ec551f # frozen: v3.95.8 + rev: f714bf454f350590f4a24c3ddb1aef02c35bf5b6 # frozen: v3.97.5 hooks: - id: trufflehog # Resolve the repo root via git-common-dir so this works in both normal repos and worktrees @@ -53,7 +53,7 @@ repos: hooks: - id: openapi-spec-validator - repo: https://github.com/streetsidesoftware/cspell-cli - rev: ea11f9efc0bec520073405bc30552da887ba71bc # frozen: v10.0.1 + rev: 058f44e47719e80d8f1e597a235310b60e937860 # frozen: v10.2.0 hooks: - id: cspell types: [markdown] diff --git a/{{cookiecutter.project_name}}/scripts/validate_service_definition.py b/{{cookiecutter.project_name}}/scripts/validate_service_definition.py index 7f43223..74d4cc1 100755 --- a/{{cookiecutter.project_name}}/scripts/validate_service_definition.py +++ b/{{cookiecutter.project_name}}/scripts/validate_service_definition.py @@ -11,7 +11,7 @@ import sys from pathlib import Path -from typing import Literal, Optional +from typing import Literal import yaml from pydantic import ( @@ -82,10 +82,10 @@ class InfrastructureDependency(BaseModel): "search_engine", ] ] - local_runtime: Optional[Literal["compose", "process", "managed", "mock"]] = Field( + local_runtime: Literal["compose", "process", "managed", "mock"] | None = Field( None, description="How to accommodate the infrastructure locally" ) - version: Optional[str] = Field( + version: str | None = Field( None, pattern=r"^(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$" ) @@ -94,7 +94,7 @@ class ServiceDependency(BaseModel): model_config = ConfigDict(strict=True) name: str - version: Optional[str] = Field(None, pattern=r"\b[0-9a-f]{40}\b") + version: str | None = Field(None, pattern=r"\b[0-9a-f]{40}\b") class DependsOnConfig(BaseModel): @@ -137,7 +137,7 @@ class DevelopConfig(BaseModel): model_config = ConfigDict(strict=True) software: SoftwareConfig - depends_on: Optional[DependsOnConfig] = Field( + depends_on: DependsOnConfig | None = Field( default_factory=DependsOnConfig, description="The dependencies of this service during development", ) @@ -147,7 +147,7 @@ class PublishConfig(BaseModel): model_config = ConfigDict(strict=True) publish_type: Literal["docker", "oci", "archive", "package", "binary", "none"] - depends_on: Optional[DependsOnConfig] = Field( + depends_on: DependsOnConfig | None = Field( default_factory=DependsOnConfig, description="The dependencies of this service in order to build and publish", ) @@ -166,7 +166,7 @@ class DeployConfig(BaseModel): "ansible", "none", ] - depends_on: Optional[DependsOnConfig] = Field( + depends_on: DependsOnConfig | None = Field( default_factory=DependsOnConfig, description="The dependencies of this service in order to deploy", ) @@ -194,7 +194,7 @@ class OperationsScheduleConfig(BaseModel): uptime: Literal["continuous", "on_demand", "scheduled", "business_hours"] = Field( "continuous", description="The uptime expectations" ) - days: Optional[ + days: ( list[ Literal[ "Monday", @@ -206,27 +206,28 @@ class OperationsScheduleConfig(BaseModel): "Sunday", ] ] - ] = None - start: Optional[str] = None # RFC3339 timestamp - end: Optional[str] = None # RFC3339 timestamp + | None + ) = None + start: str | None = None # RFC3339 timestamp + end: str | None = None # RFC3339 timestamp @field_validator("days", "start", "end", mode="before") @classmethod def validate_scheduled_fields(cls, value, info): - if info.data.get("uptime") == "scheduled": - if not value: - raise ValueError( - f'{info.field_name} must be specified if uptime is set to "scheduled"' - ) + if info.data.get("uptime") == "scheduled" and not value: + raise ValueError( + f'{info.field_name} must be specified if uptime is set to "scheduled"' + ) return value @model_validator(mode="after") def validate_no_extra_fields(self): - if self.uptime != "scheduled": - if self.days is not None or self.start is not None or self.end is not None: - raise ValueError( - 'days, start, and end should only be set if uptime is "scheduled"' - ) + if self.uptime != "scheduled" and ( + self.days is not None or self.start is not None or self.end is not None + ): + raise ValueError( + 'days, start, and end should only be set if uptime is "scheduled"' + ) return self @@ -234,21 +235,21 @@ class RuntimeEnvironmentConfig(BaseModel): model_config = ConfigDict(strict=True) authorization: dict[Literal["public"], bool] - service_route_prefix: Optional[str] = Field( + service_route_prefix: str | None = Field( None, description="The service-specific route prefix, like data for /api/data/example and /api/data", ) - stage_name: Optional[str] = Field( + stage_name: str | None = Field( None, description="The service-specific stage name, like current or v1; typically used for REST APIs", ) uptime_sla: float = Field(95.0, description="The uptime agreement") uptime_slo: float = Field(99.0, description="The uptime objective") - operations_schedule: Optional[OperationsScheduleConfig] = Field( + operations_schedule: OperationsScheduleConfig | None = Field( default_factory=lambda: OperationsScheduleConfig(uptime="on_demand"), description="The operations schedule", ) - depends_on: Optional[DependsOnConfig] = Field( + depends_on: DependsOnConfig | None = Field( default_factory=DependsOnConfig, description="Dependencies for the specific environment", ) @@ -347,11 +348,11 @@ class ServiceSchema(BaseModel): develop: DevelopConfig = Field( description="Context used when updating the service's software, tests, documentation, IaC, or other related information." ) - publish: Optional[PublishConfig] = Field( + publish: PublishConfig | None = Field( None, description="Details regarding how and where artifacts are published, such as container images and in-toto attestations", ) - deploy: Optional[DeployConfig] = Field( + deploy: DeployConfig | None = Field( None, description="Details regarding how and where to deploy a service" ) runtime: RuntimeConfig = Field( @@ -373,14 +374,15 @@ def check_lifecycle_constraints(self): "archived", ] - if self.deploy and self.deploy.environments: - if ( - lifecycle in production_restricted_lifecycles - and "production" in self.deploy.environments - ): - raise ValueError( - f"Service {self.name}'s lifecycle of '{lifecycle}' is not allowed to deploy to production." - ) + if ( + self.deploy + and self.deploy.environments + and lifecycle in production_restricted_lifecycles + and "production" in self.deploy.environments + ): + raise ValueError( + f"Service {self.name}'s lifecycle of '{lifecycle}' is not allowed to deploy to production." + ) return self @@ -407,8 +409,11 @@ def validate_service_definition(file_path: Path) -> bool: location = " -> ".join(str(loc) for loc in error["loc"]) print(f" - {location}: {error['msg']}") return False - except Exception as e: - print(f"Unexpected error: {e}") + except OSError as e: + print(f"Error reading {file_path}: {e}") + return False + except TypeError as e: + print(f"Error: {file_path} must contain a YAML mapping at the top level: {e}") return False diff --git a/{{cookiecutter.project_name}}/tests/test_integration.py b/{{cookiecutter.project_name}}/tests/test_integration.py index 39920c2..6eff472 100755 --- a/{{cookiecutter.project_name}}/tests/test_integration.py +++ b/{{cookiecutter.project_name}}/tests/test_integration.py @@ -4,9 +4,9 @@ """ import json -import tomllib import os import subprocess +import tomllib from pathlib import Path import pytest @@ -132,6 +132,7 @@ def test_docker_image(): process = subprocess.run( ["docker", "run", "--rm", f"{image_name}:latest"], capture_output=True, + check=False, cwd=project_root, ) assert process.returncode == 1, ( @@ -154,6 +155,7 @@ def test_docker_image(): process = subprocess.run( command, capture_output=True, + check=False, cwd=project_root, ) assert process.returncode == expected_exit, ( diff --git a/{{cookiecutter.project_name}}/tests/test_main.py b/{{cookiecutter.project_name}}/tests/test_main.py index 9414b00..c6e5444 100755 --- a/{{cookiecutter.project_name}}/tests/test_main.py +++ b/{{cookiecutter.project_name}}/tests/test_main.py @@ -22,9 +22,8 @@ def test_main_function(): """Test that main() raises NotImplementedError""" from main import main - with patch("sys.argv", ["main"]): - with pytest.raises(NotImplementedError): - main() + with patch("sys.argv", ["main"]), pytest.raises(NotImplementedError): + main() @pytest.mark.unit @@ -46,6 +45,7 @@ def test_main_as_script(): result = subprocess.run( [sys.executable, str(main_path)], capture_output=True, + check=False, text=True, ) @@ -62,6 +62,7 @@ def test_main_as_script_version(): result = subprocess.run( [sys.executable, str(main_path), "--version"], capture_output=True, + check=True, text=True, ) diff --git a/{{cookiecutter.project_name}}/tests/test_package.py b/{{cookiecutter.project_name}}/tests/test_package.py index f32c476..f5a8b1b 100755 --- a/{{cookiecutter.project_name}}/tests/test_package.py +++ b/{{cookiecutter.project_name}}/tests/test_package.py @@ -6,15 +6,15 @@ import pytest from {{ cookiecutter.project_slug }} import ( - __maintainer__, - __project_name__, - __version__, -{%- if cookiecutter.license != 'NONE' %} - __license__, -{%- endif %} {%- if cookiecutter.license == 'NONE' %} __copyright__, {%- endif %} +{%- if cookiecutter.license != 'NONE' %} + __license__, +{%- endif %} + __maintainer__, + __project_name__, + __version__, )