From 36467d786c250ce5b522b062431f2d4adfe03767 Mon Sep 17 00:00:00 2001 From: Luke Hinds Date: Thu, 21 Nov 2024 11:21:37 +0000 Subject: [PATCH 1/4] Implement Basic CI This change introduces a Makefile, which provides tests: `pytest --cov=codegate --cov-report=term-missing` security: `bandit -r src/` lint: `ruff check .` format: `black . && isort .` Along with `install` and `build`. These are then all surfaced within GitHub Actions, along with codecov upload A release file is there, but this may be removed, amended, depending upon are agreed packaging approach at launch Closes: #16 --- .github/workflows/ci.yml | 41 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 41 +++++++++++++++++++++++++++++++++++ Makefile | 30 +++++++++++++++++++++++++ README.md | 3 +++ pyproject.toml | 9 ++++++-- src/codegate/cli.py | 6 ++--- src/codegate/config.py | 5 +++-- src/codegate/logging.py | 6 ++++- tests/conftest.py | 24 ++++++++++---------- tests/test_cli.py | 17 +++++++++------ tests/test_config.py | 10 ++++----- tests/test_logging.py | 7 +++--- 12 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 Makefile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..da879255 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ".[dev]" + + - name: Run linting + run: make lint + + - name: Run security checks + run: make security + + - name: Run tests + run: make test + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + fail_ci_if_error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..3189ca4e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ".[dev]" + + - name: Run tests + run: make test + + - name: Build package + run: make build + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: dist/* + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..2fe10736 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +.PHONY: clean install format lint test security build all + +clean: + rm -rf build/ + rm -rf dist/ + rm -rf *.egg-info + rm -f .coverage + find . -type d -name '__pycache__' -exec rm -rf {} + + find . -type f -name '*.pyc' -delete + +install: + pip install -e ".[dev]" + +format: + black . + isort . + +lint: + ruff check . + +test: + pytest --cov=codegate --cov-report=term-missing + +security: + bandit -r src/ + +build: clean test + python -m build + +all: clean install format lint test security build diff --git a/README.md b/README.md index d9df65dc..0427e873 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Codegate +[![CI](https://github.com/stacklok/codegate/actions/workflows/ci.yml/badge.svg)](https://github.com/stacklok/codegate/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/stacklok/codegate/branch/main/graph/badge.svg)](https://codecov.io/gh/stacklok/codegate) + A configurable Generative AI gateway, protecting developers from the dangers of AI. ## Features diff --git a/pyproject.toml b/pyproject.toml index df71b4b8..f3e794f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,10 @@ dev = [ "pytest>=7.4.0", "pytest-cov>=4.1.0", "black>=23.7.0", - "ruff>=0.0.284", + "ruff>=0.7.4", + "bandit>=1.7.10", + "build>=1.0.0", + "wheel>=0.40.0", ] [build-system] @@ -28,11 +31,13 @@ line-length = 88 target-version = ["py310"] [tool.ruff] -select = ["E", "F", "I", "N", "W"] line-length = 88 target-version = "py310" fix = true +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W"] + [project.scripts] codegate = "codegate.cli:main" diff --git a/src/codegate/cli.py b/src/codegate/cli.py index a32c78d2..0cae7601 100644 --- a/src/codegate/cli.py +++ b/src/codegate/cli.py @@ -7,7 +7,7 @@ import click -from .config import Config, ConfigurationError, LogLevel, LogFormat +from .config import Config, ConfigurationError, LogFormat, LogLevel from .logging import setup_logging @@ -64,7 +64,7 @@ def serve( config: Optional[Path], ) -> None: """Start the codegate server.""" - + try: # Load configuration with priority resolution cfg = Config.load( @@ -92,7 +92,7 @@ def serve( "log_format": cfg.log_format.value, } ) - + # TODO: Jakub Implement actual server logic here logger.info("Server started successfully") diff --git a/src/codegate/config.py b/src/codegate/config.py index fba1bb22..0af4cbdd 100644 --- a/src/codegate/config.py +++ b/src/codegate/config.py @@ -1,10 +1,11 @@ """Configuration management for codegate.""" +import os from dataclasses import dataclass from enum import Enum from pathlib import Path from typing import Optional, Union -import os + import yaml @@ -66,7 +67,7 @@ def __post_init__(self) -> None: """Validate configuration after initialization.""" if not isinstance(self.port, int) or not (1 <= self.port <= 65535): raise ConfigurationError("Port must be between 1 and 65535") - + if not isinstance(self.log_level, LogLevel): try: self.log_level = LogLevel(self.log_level) diff --git a/src/codegate/logging.py b/src/codegate/logging.py index 8ba0fe66..6a708001 100644 --- a/src/codegate/logging.py +++ b/src/codegate/logging.py @@ -90,7 +90,11 @@ def __init__(self) -> None: datefmt="%Y-%m-%dT%H:%M:%S.%03dZ" ) - def formatTime(self, record: logging.LogRecord, datefmt: Optional[str] = None) -> str: + def formatTime( # noqa: N802 + self, + record: logging.LogRecord, + datefmt: Optional[str] = None + ) -> str: """Format the time with millisecond precision. Args: diff --git a/tests/conftest.py b/tests/conftest.py index 32e48764..aa7f0478 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,11 +5,11 @@ import os from pathlib import Path from typing import Any, Generator, Iterator -import pytest -import yaml from unittest.mock import patch -from codegate.config import Config, LogFormat +import pytest +import yaml +from codegate.config import Config @pytest.fixture @@ -22,10 +22,10 @@ def temp_config_file(tmp_path: Path) -> Iterator[Path]: "log_format": "JSON" } config_file = tmp_path / "config.yaml" - + with open(config_file, "w") as f: yaml.dump(config_data, f) - + yield config_file @@ -33,16 +33,16 @@ def temp_config_file(tmp_path: Path) -> Iterator[Path]: def env_vars() -> Generator[None, None, None]: """Set up test environment variables.""" original_env = dict(os.environ) - + os.environ.update({ "CODEGATE_APP_PORT": "8989", "CODEGATE_APP_HOST": "localhost", "CODEGATE_APP_LOG_LEVEL": "WARNING", "CODEGATE_LOG_FORMAT": "TEXT" }) - + yield - + # Restore original environment os.environ.clear() os.environ.update(original_env) @@ -58,7 +58,7 @@ def default_config() -> Config: def mock_datetime() -> Generator[None, None, None]: """Mock datetime to return a fixed time.""" fixed_dt = datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.UTC) - + with patch("datetime.datetime") as mock_dt: mock_dt.now.return_value = fixed_dt mock_dt.fromtimestamp.return_value = fixed_dt @@ -70,15 +70,15 @@ def mock_datetime() -> Generator[None, None, None]: def capture_logs(tmp_path: Path) -> Iterator[Path]: """Capture logs to a file for testing.""" log_file = tmp_path / "test.log" - + # Create a file handler import logging handler = logging.FileHandler(log_file) logger = logging.getLogger() logger.addHandler(handler) - + yield log_file - + # Clean up handler.close() logger.removeHandler(handler) diff --git a/tests/test_cli.py b/tests/test_cli.py index df098157..a3762dad 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,9 +6,8 @@ import pytest from click.testing import CliRunner - from codegate.cli import cli -from codegate.config import LogLevel, LogFormat +from codegate.config import LogFormat, LogLevel @pytest.fixture @@ -35,7 +34,7 @@ def test_serve_default_options(cli_runner: CliRunner, mock_logging: Any) -> None with patch("logging.getLogger") as mock_logger: logger_instance = mock_logger.return_value result = cli_runner.invoke(cli, ["serve"]) - + assert result.exit_code == 0 mock_logging.assert_called_once_with(LogLevel.INFO, LogFormat.JSON) logger_instance.info.assert_any_call( @@ -63,7 +62,7 @@ def test_serve_custom_options(cli_runner: CliRunner, mock_logging: Any) -> None: "--log-format", "TEXT" ] ) - + assert result.exit_code == 0 mock_logging.assert_called_once_with(LogLevel.DEBUG, LogFormat.TEXT) logger_instance.info.assert_any_call( @@ -95,12 +94,16 @@ def test_serve_invalid_log_level(cli_runner: CliRunner) -> None: assert "Invalid value for '--log-level'" in result.output -def test_serve_with_config_file(cli_runner: CliRunner, mock_logging: Any, temp_config_file: Path) -> None: +def test_serve_with_config_file( + cli_runner: CliRunner, + mock_logging: Any, + temp_config_file: Path +) -> None: """Test serve command with config file.""" with patch("logging.getLogger") as mock_logger: logger_instance = mock_logger.return_value result = cli_runner.invoke(cli, ["serve", "--config", str(temp_config_file)]) - + assert result.exit_code == 0 mock_logging.assert_called_once_with(LogLevel.DEBUG, LogFormat.JSON) logger_instance.info.assert_any_call( @@ -140,7 +143,7 @@ def test_serve_priority_resolution( "--log-format", "TEXT" ] ) - + assert result.exit_code == 0 mock_logging.assert_called_once_with(LogLevel.ERROR, LogFormat.TEXT) logger_instance.info.assert_any_call( diff --git a/tests/test_config.py b/tests/test_config.py index 04b6b660..ad5da7ab 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,10 +2,10 @@ import os from pathlib import Path + import pytest import yaml - -from codegate.config import Config, ConfigurationError, LogLevel, LogFormat +from codegate.config import Config, ConfigurationError, LogFormat, LogLevel def test_default_config(default_config: Config) -> None: @@ -30,7 +30,7 @@ def test_config_from_invalid_file(tmp_path: Path) -> None: invalid_file = tmp_path / "invalid.yaml" with open(invalid_file, "w") as f: f.write("invalid: yaml: content") - + with pytest.raises(ConfigurationError): Config.from_file(invalid_file) @@ -106,13 +106,13 @@ def test_log_format_case_insensitive(tmp_path: Path) -> None: config_file = tmp_path / "config.yaml" with open(config_file, "w") as f: yaml.dump({"log_format": "json"}, f) - + config = Config.from_file(config_file) assert config.log_format == LogFormat.JSON with open(config_file, "w") as f: yaml.dump({"log_format": "TEXT"}, f) - + config = Config.from_file(config_file) assert config.log_format == LogFormat.TEXT diff --git a/tests/test_logging.py b/tests/test_logging.py index d526fdd2..03f03b87 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,9 +1,10 @@ -import logging import json -import pytest +import logging from io import StringIO -from codegate.logging import JSONFormatter, TextFormatter, setup_logging + from codegate.config import LogFormat, LogLevel +from codegate.logging import JSONFormatter, TextFormatter, setup_logging + def test_json_formatter(): log_record = logging.LogRecord( From f67628414f2cbccc5267b33dd20a682a1fd0b066 Mon Sep 17 00:00:00 2001 From: Luke Hinds Date: Thu, 21 Nov 2024 11:33:15 +0000 Subject: [PATCH 2/4] python versions --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da879255..19a8f10f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11"] steps: - uses: actions/checkout@v3 From e5c098f979f59e8bd3af614950b32645acc8206e Mon Sep 17 00:00:00 2001 From: Luke Hinds Date: Thu, 21 Nov 2024 11:57:55 +0000 Subject: [PATCH 3/4] CI failing, no point supporting old releases --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19a8f10f..532b7bfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11"] + python-version: ["3.11", "3.12"] steps: - uses: actions/checkout@v3 From 2023b8f82a842615e5dc6647e6f17f0909c56e9c Mon Sep 17 00:00:00 2001 From: Luke Hinds Date: Thu, 21 Nov 2024 16:31:32 +0000 Subject: [PATCH 4/4] Remove codecov --- .github/workflows/ci.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 532b7bfb..f95da4af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,22 +20,18 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - + - name: Install dependencies run: | python -m pip install --upgrade pip pip install ".[dev]" - + - name: Run linting run: make lint - + - name: Run security checks run: make security - + - name: Run tests run: make test - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - fail_ci_if_error: true +