Skip to content
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
2 changes: 1 addition & 1 deletion docs/tutorials/codebase-analytics-dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ icon: "calculator"
iconType: "solid"
---

This tutorial explains how codebase metrics are effiently calculated using the `codegen` library in the Codebase Analytics Dashboard. The metrics include indeces of codebase maintainabilith and complexity.
This tutorial explains how codebase metrics are efficiently calculated using the `codegen` library in the Codebase Analytics Dashboard. The metrics include indices of codebase maintainabilith and complexity.

View the full code and setup instructions in our [codebase-analytics repository](https://github.com/codegen-sh/codebase-analytics).

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ dependencies = [
"langchain-anthropic>=0.3.7",
"lox>=0.12.0",
"httpx>=0.28.1",
"docker>=6.1.3",
]

license = { text = "Apache-2.0" }
Expand Down
32 changes: 32 additions & 0 deletions src/codegen/cli/commands/start/docker_container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import docker


class DockerContainer:
_client: docker.DockerClient
host: str | None
port: int | None
name: str

def __init__(self, client: docker.DockerClient, name: str, port: int | None = None, host: str | None = None):
self._client = client
self.host = host
self.port = port
self.name = name

def is_running(self) -> bool:
try:
container = self._client.containers.get(self.name)
return container.status == "running"
except docker.errors.NotFound:
return False

def start(self) -> bool:
try:
container = self._client.containers.get(self.name)
container.start()
return True
except (docker.errors.NotFound, docker.errors.APIError):
return False

def __str__(self) -> str:
return f"DockerSession(name={self.name}, host={self.host or 'unknown'}, port={self.port or 'unknown'})"
46 changes: 46 additions & 0 deletions src/codegen/cli/commands/start/docker_fleet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import docker

from codegen.cli.commands.start.docker_container import DockerContainer

CODEGEN_RUNNER_IMAGE = "codegen-runner"


class DockerFleet:
containers: list[DockerContainer]

def __init__(self, containers: list[DockerContainer]):
self.containers = containers

@classmethod
def load(cls) -> "DockerFleet":
try:
client = docker.from_env()
containers = client.containers.list(all=True, filters={"ancestor": CODEGEN_RUNNER_IMAGE})
codegen_containers = []
for container in containers:
if container.attrs["Config"]["Image"] == CODEGEN_RUNNER_IMAGE:
if container.status == "running":
host_config = next(iter(container.ports.values()))[0]
codegen_container = DockerContainer(client=client, host=host_config["HostIp"], port=host_config["HostPort"], name=container.name)
else:
codegen_container = DockerContainer(client=client, name=container.name)
codegen_containers.append(codegen_container)

return cls(containers=codegen_containers)
except docker.errors.NotFound:
return cls(containers=[])

@property
def active_containers(self) -> list[DockerContainer]:
return [container for container in self.containers if container.is_running()]

def get(self, name: str) -> DockerContainer | None:
return next((container for container in self.containers if container.name == name), None)

def __str__(self) -> str:
return f"DockerFleet(containers={',\n'.join(str(container) for container in self.containers)})"


if __name__ == "__main__":
pool = DockerFleet.load()
print(pool)
53 changes: 40 additions & 13 deletions src/codegen/cli/commands/start/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,27 @@
from rich.box import ROUNDED
from rich.panel import Panel

from codegen.cli.commands.start.docker_container import DockerContainer
from codegen.cli.commands.start.docker_fleet import CODEGEN_RUNNER_IMAGE, DockerFleet
from codegen.configs.models.secrets import SecretsConfig
from codegen.git.repo_operator.local_git_repo import LocalGitRepo
from codegen.git.schemas.repo_config import RepoConfig
from codegen.shared.network.port import get_free_port

_default_host = "0.0.0.0"


@click.command(name="start")
@click.option("--platform", "-t", type=click.Choice(["linux/amd64", "linux/arm64", "linux/amd64,linux/arm64"]), default="linux/amd64,linux/arm64", help="Target platform(s) for the Docker image")
@click.option("--port", "-p", type=int, default=None, help="Port to run the server on")
@click.option("--detached", "-d", is_flag=True, default=False, help="Starts up the server as detached background process")
def start_command(port: int | None, platform: str, detached: bool):
def start_command(port: int | None, platform: str):
"""Starts a local codegen server"""
repo_path = Path.cwd().resolve()
repo_config = RepoConfig.from_repo_path(str(repo_path))
fleet = DockerFleet.load()
if (container := fleet.get(repo_config.name)) is not None:
return _handle_existing_container(repo_config, container)

codegen_version = version("codegen")
rich.print(f"[bold green]Codegen version:[/bold green] {codegen_version}")
codegen_root = Path(__file__).parent.parent.parent.parent.parent.parent
Expand All @@ -29,8 +38,9 @@ def start_command(port: int | None, platform: str, detached: bool):
rich.print("[bold blue]Building Docker image...[/bold blue]")
_build_docker_image(codegen_root, platform)
rich.print("[bold blue]Starting Docker container...[/bold blue]")
_run_docker_container(port, detached)
rich.print(Panel(f"[green]Server started successfully![/green]\nAccess the server at: [bold]http://0.0.0.0:{port}[/bold]", box=ROUNDED, title="Codegen Server"))
_run_docker_container(repo_config, port)
rich.print(Panel(f"[green]Server started successfully![/green]\nAccess the server at: [bold]http://{_default_host}:{port}[/bold]", box=ROUNDED, title="Codegen Server"))
# TODO: memory snapshot here
except subprocess.CalledProcessError as e:
rich.print(f"[bold red]Error:[/bold red] Failed to {e.cmd[0]} Docker container")
raise click.Abort()
Expand All @@ -39,7 +49,26 @@ def start_command(port: int | None, platform: str, detached: bool):
raise click.Abort()


def _build_docker_image(codegen_root: Path, platform: str):
def _handle_existing_container(repo_config: RepoConfig, container: DockerContainer) -> None:
if container.is_running():
rich.print(
Panel(
f"[green]Codegen server for {repo_config.name} is already running at: [bold]http://{container.host}:{container.port}[/bold][/green]",
box=ROUNDED,
title="Codegen Server",
)
)
return

if container.start():
rich.print(Panel(f"[yellow]Docker container for {repo_config.name} is not running. Restarting...[/yellow]", box=ROUNDED, title="Docker Session"))
return

rich.print(Panel(f"[red]Failed to restart container for {repo_config.name}[/red]", box=ROUNDED, title="Docker Session"))
click.Abort()


def _build_docker_image(codegen_root: Path, platform: str) -> None:
build_cmd = [
"docker",
"buildx",
Expand All @@ -57,21 +86,19 @@ def _build_docker_image(codegen_root: Path, platform: str):
subprocess.run(build_cmd, check=True)


def _run_docker_container(port: int, detached: bool):
repo_path = Path.cwd().resolve()
repo_config = RepoConfig.from_repo_path(repo_path)
def _run_docker_container(repo_config: RepoConfig, port: int) -> None:
container_repo_path = f"/app/git/{repo_config.name}"
name_args = ["--name", f"{repo_config.name}"]
envvars = {
"REPOSITORY_LANGUAGE": repo_config.language.value,
"REPOSITORY_OWNER": LocalGitRepo(repo_path).owner,
"REPOSITORY_OWNER": LocalGitRepo(repo_config.repo_path).owner,
"REPOSITORY_PATH": container_repo_path,
"GITHUB_TOKEN": SecretsConfig().github_token,
}
envvars_args = [arg for k, v in envvars.items() for arg in ("--env", f"{k}={v}")]
mount_args = ["-v", f"{repo_path}:{container_repo_path}"]
run_mode = "-d" if detached else "-it"
entry_point = f"uv run --frozen uvicorn codegen.runner.sandbox.server:app --host 0.0.0.0 --port {port}"
run_cmd = ["docker", "run", run_mode, "-p", f"{port}:{port}", *mount_args, *envvars_args, "codegen-runner", entry_point]
mount_args = ["-v", f"{repo_config.repo_path}:{container_repo_path}"]
entry_point = f"uv run --frozen uvicorn codegen.runner.sandbox.server:app --host {_default_host} --port {port}"
run_cmd = ["docker", "run", "-d", "-p", f"{port}:{port}", *name_args, *mount_args, *envvars_args, CODEGEN_RUNNER_IMAGE, entry_point]

rich.print(f"run_cmd: {str.join(' ', run_cmd)}")
subprocess.run(run_cmd, check=True)
2 changes: 1 addition & 1 deletion src/codegen/extensions/clients/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def comment_on_issue(self, issue_id: str, body: str) -> dict:
comment_data = data["data"]["commentCreate"]["comment"]

return comment_data
except:
except Exception:
msg = f"Error creating comment\n{data}"
raise Exception(msg)

Expand Down
47 changes: 47 additions & 0 deletions src/codegen/runner/clients/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Client used to abstract the weird stdin/stdout communication we have with the sandbox"""

import logging

import requests
from fastapi import params

logger = logging.getLogger(__name__)

DEFAULT_SERVER_PORT = 4002

EPHEMERAL_SERVER_PATH = "codegen.runner.sandbox.ephemeral_server:app"


class Client:
"""Client for interacting with the sandbox server."""

host: str
port: int
base_url: str

def __init__(self, host: str, port: int) -> None:
self.host = host
self.port = port
self.base_url = f"http://{host}:{port}"

def healthcheck(self, raise_on_error: bool = True) -> bool:
try:
self.get("/")
return True
except requests.exceptions.ConnectionError:
if raise_on_error:
raise
return False

def get(self, endpoint: str, data: dict | None = None) -> requests.Response:
url = f"{self.base_url}{endpoint}"
response = requests.get(url, json=data)
response.raise_for_status()
return response

def post(self, endpoint: str, data: dict | None = None, authorization: str | params.Header | None = None) -> requests.Response:
url = f"{self.base_url}{endpoint}"
headers = {"Authorization": str(authorization)} if authorization else None
response = requests.post(url, json=data, headers=headers)
response.raise_for_status()
return response
56 changes: 49 additions & 7 deletions src/codegen/runner/clients/codebase_client.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,70 @@
"""Client used to abstract the weird stdin/stdout communication we have with the sandbox"""

import logging
import os
import subprocess
import time

from codegen.configs.models.secrets import SecretsConfig
from codegen.git.schemas.repo_config import RepoConfig
from codegen.runner.clients.server_client import LocalServerClient
from codegen.runner.clients.client import Client
from codegen.runner.models.apis import SANDBOX_SERVER_PORT

logger = logging.getLogger(__name__)

DEFAULT_SERVER_PORT = 4002
EPHEMERAL_SERVER_PATH = "codegen.runner.sandbox.ephemeral_server:app"
RUNNER_SERVER_PATH = "codegen.runner.sandbox.server:app"


class CodebaseClient(LocalServerClient):
logger = logging.getLogger(__name__)


class CodebaseClient(Client):
"""Client for interacting with the locally hosted sandbox server."""

repo_config: RepoConfig

def __init__(self, repo_config: RepoConfig, host: str = "127.0.0.1", port: int = SANDBOX_SERVER_PORT):
def __init__(self, repo_config: RepoConfig, host: str = "127.0.0.1", port: int = SANDBOX_SERVER_PORT, server_path: str = RUNNER_SERVER_PATH):
super().__init__(host=host, port=port)
self.repo_config = repo_config
super().__init__(server_path=RUNNER_SERVER_PATH, host=host, port=port)
self._process = None
self._start_server(server_path)

def __del__(self):
"""Cleanup the subprocess when the client is destroyed"""
if self._process is not None:
self._process.terminate()
self._process.wait()

def _start_server(self, server_path: str) -> None:
"""Start the FastAPI server in a subprocess"""
envs = self._get_envs()
logger.info(f"Starting local server on {self.base_url} with envvars: {envs}")

self._process = subprocess.Popen(

Check failure on line 43 in src/codegen/runner/clients/codebase_client.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible types in assignment (expression has type "Popen[bytes]", variable has type "None") [assignment]
[
"uvicorn",
server_path,
"--host",
self.host,
"--port",
str(self.port),
],
env=envs,
)
self._wait_for_server()

def _wait_for_server(self, timeout: int = 30, interval: float = 0.3) -> None:
"""Wait for the server to start by polling the health endpoint"""
start_time = time.time()
while (time.time() - start_time) < timeout:
if self.healthcheck(raise_on_error=False):
return
time.sleep(interval)
msg = "Server failed to start within timeout period"
raise TimeoutError(msg)

def _get_envs(self) -> dict:
envs = super()._get_envs()
envs = os.environ.copy()
codebase_envs = {
"REPOSITORY_LANGUAGE": self.repo_config.language.value,
"REPOSITORY_OWNER": self.repo_config.organization_name,
Expand All @@ -30,7 +72,7 @@
"GITHUB_TOKEN": SecretsConfig().github_token,
}

envs.update(codebase_envs)

Check failure on line 75 in src/codegen/runner/clients/codebase_client.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "update" of "MutableMapping" has incompatible type "dict[str, str | None]"; expected "SupportsKeysAndGetItem[str, str]" [arg-type]
return envs


Expand Down
31 changes: 31 additions & 0 deletions src/codegen/runner/clients/docker_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Client for interacting with the locally hosted sandbox server hosted on a docker container."""

from codegen.cli.commands.start.docker_container import DockerContainer
from codegen.cli.commands.start.docker_fleet import DockerFleet
from codegen.runner.clients.client import Client
from codegen.runner.models.apis import DIFF_ENDPOINT, GetDiffRequest
from codegen.runner.models.codemod import Codemod


class DockerClient(Client):
"""Client for interacting with the locally hosted sandbox server hosted on a docker container."""

def __init__(self, container: DockerContainer):
if not container.is_running() or container.host is None or container.port is None:
msg = f"Container {container.name} is not running."
raise Exception(msg)
super().__init__(container.host, container.port)


if __name__ == "__main__":
fleet = DockerFleet.load()
cur = next((container for container in fleet.containers if container.is_running()), None)
if cur is None:
msg = "No running container found. Run `codegen start` from a git repo first."
raise Exception(msg)
client = DockerClient(cur)
print(f"healthcheck: {client.healthcheck()}")
codemod = Codemod(user_code="print(codebase)")
diff_req = GetDiffRequest(codemod=codemod)
res = client.post(DIFF_ENDPOINT, diff_req.model_dump())
print(res.json())
Loading
Loading