Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Implement Basic CI #48

Merged
merged 4 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]

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

41 changes: 41 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}
30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"

Expand Down
6 changes: 3 additions & 3 deletions src/codegate/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -64,7 +64,7 @@ def serve(
config: Optional[Path],
) -> None:
"""Start the codegate server."""

try:
# Load configuration with priority resolution
cfg = Config.load(
Expand Down Expand Up @@ -92,7 +92,7 @@ def serve(
"log_format": cfg.log_format.value,
}
)

# TODO: Jakub Implement actual server logic here
logger.info("Server started successfully")

Expand Down
5 changes: 3 additions & 2 deletions src/codegate/config.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion src/codegate/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
24 changes: 12 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,27 +22,27 @@ 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


@pytest.fixture
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)
Expand All @@ -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
Expand All @@ -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)
Expand Down
17 changes: 10 additions & 7 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 5 additions & 5 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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

Expand Down
7 changes: 4 additions & 3 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
Loading