Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 5 additions & 0 deletions .codegen/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
[secrets]
github_token = ""
openai_api_key = ""

[repository]
organization_name = "codegen-sh"
repo_name = "codegen-sdk"
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
"watchfiles<1.1.0,>=1.0.0",
"rich<14.0.0,>=13.7.1",
"pydantic<3.0.0,>=2.9.2",
"pydantic-settings>=2.0.0",
"docstring-parser<1.0,>=0.16",
"plotly<6.0.0,>=5.24.0",
"humanize<5.0.0,>=4.10.0",
Expand Down
2 changes: 2 additions & 0 deletions src/codegen/cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import rich_click as click
from rich.traceback import install

from codegen.cli.commands.config.main import config_command
from codegen.cli.commands.create.main import create_command
from codegen.cli.commands.deploy.main import deploy_command
from codegen.cli.commands.expert.main import expert_command
Expand Down Expand Up @@ -39,6 +40,7 @@ def main():
main.add_command(run_on_pr_command)
main.add_command(notebook_command)
main.add_command(reset_command)
main.add_command(config_command)


if __name__ == "__main__":
Expand Down
86 changes: 86 additions & 0 deletions src/codegen/cli/commands/config/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import logging
from itertools import groupby

import rich
import rich_click as click
from rich.table import Table

from codegen.shared.configs.config import config


@click.group(name="config")
def config_command():
"""Manage codegen configuration."""
pass


@config_command.command(name="list")
def list_command():
"""List current configuration values."""
table = Table(title="Configuration Values", border_style="blue", show_header=True)
table.add_column("Key", style="cyan", no_wrap=True)
table.add_column("Value", style="magenta")

def flatten_dict(data: dict, prefix: str = "") -> dict:
items = {}
for key, value in data.items():
full_key = f"{prefix}{key}" if prefix else key
if isinstance(value, dict):
# Always include dictionary fields, even if empty
if not value:
items[full_key] = "{}"
items.update(flatten_dict(value, f"{full_key}."))
else:
items[full_key] = value
return items

# Get flattened config and sort by keys
flat_config = flatten_dict(config.model_dump())
sorted_items = sorted(flat_config.items(), key=lambda x: x[0])

# Group by top-level prefix
def get_prefix(item):
return item[0].split(".")[0]

for prefix, group in groupby(sorted_items, key=get_prefix):
table.add_section()
table.add_row(f"[bold yellow]{prefix}[/bold yellow]", "")
for key, value in group:
# Remove the prefix from the displayed key
display_key = key[len(prefix) + 1 :] if "." in key else key
table.add_row(f" {display_key}", str(value))

rich.print(table)


@config_command.command(name="get")
@click.argument("key")
def get_command(key: str):
"""Get a configuration value."""
value = config.get(key)
if value is None:
rich.print(f"[red]Error: Configuration key '{key}' not found[/red]")
return

rich.print(f"[cyan]{key}[/cyan] = [magenta]{value}[/magenta]")


@config_command.command(name="set")
@click.argument("key")
@click.argument("value")
def set_command(key: str, value: str):
"""Set a configuration value and write to config.toml."""
cur_value = config.get(key)
if cur_value is None:
rich.print(f"[red]Error: Configuration key '{key}' not found[/red]")
return

if cur_value.lower() != value.lower():
try:
config.set(key, value)
except Exception as e:
logging.exception(e)
rich.print(f"[red]{e}[/red]")
return

rich.print(f"[green]Successfully set {key}=[magenta]{value}[/magenta] and saved to config.toml[/green]")
2 changes: 0 additions & 2 deletions src/codegen/git/configs/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@
CODEGEN_BOT_NAME = "codegen-bot"
CODEGEN_BOT_EMAIL = "[email protected]"
CODEOWNERS_FILEPATHS = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"]
HIGHSIDE_REMOTE_NAME = "highside"
LOWSIDE_REMOTE_NAME = "lowside"
4 changes: 2 additions & 2 deletions src/codegen/sdk/codebase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class GSFeatureFlags(BaseModel):
model_config = ConfigDict(frozen=True)
debug: bool = False
verify_graph: bool = False
track_graph: bool = True # Track the initial graph state
track_graph: bool = False # Track the initial graph state
method_usages: bool = True
sync_enabled: bool = True
ts_dependency_manager: bool = False # Enable Typescript Dependency Manager
Expand All @@ -50,7 +50,7 @@ class GSFeatureFlags(BaseModel):

DefaultFlags = GSFeatureFlags(sync_enabled=False)

TestFlags = GSFeatureFlags(debug=True, verify_graph=True, full_range_index=True)
TestFlags = GSFeatureFlags(debug=True, track_graph=True, verify_graph=True, full_range_index=True)
LintFlags = GSFeatureFlags(method_usages=False)
ParseTestFlags = GSFeatureFlags(debug=False, track_graph=False)

Expand Down
54 changes: 54 additions & 0 deletions src/codegen/shared/configs/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from pathlib import Path

import tomllib

from codegen.shared.configs.constants import CONFIG_PATH
from codegen.shared.configs.models import Config


def load(config_path: Path | None = None) -> Config:
"""Loads configuration from various sources."""
# Load from .env file
env_config = _load_from_env()

# Load from .codegen/config.toml file
toml_config = _load_from_toml(config_path or CONFIG_PATH)

# Merge configurations recursively
config_dict = _merge_configs(env_config.model_dump(), toml_config.model_dump())

return Config(**config_dict)


def _load_from_env() -> Config:
"""Load configuration from the environment variables."""
return Config()


def _load_from_toml(config_path: Path) -> Config:
"""Load configuration from the TOML file."""
if config_path.exists():
with open(config_path, "rb") as f:
toml_config = tomllib.load(f)
return Config.model_validate(toml_config, strict=False)

return Config()


def _merge_configs(base: dict, override: dict) -> dict:
"""Recursively merge two dictionaries, with override taking precedence for non-null values."""
merged = base.copy()
for key, override_value in override.items():
if isinstance(override_value, dict) and key in base and isinstance(base[key], dict):
# Recursively merge nested dictionaries
merged[key] = _merge_configs(base[key], override_value)
elif override_value is not None and override_value != "":
# Override only if value is non-null and non-empty
merged[key] = override_value
return merged


config = load()

if __name__ == "__main__":
print(config)
11 changes: 11 additions & 0 deletions src/codegen/shared/configs/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pathlib import Path

# Config file
CODEGEN_REPO_ROOT = Path(__file__).parent.parent.parent.parent.parent
CODEGEN_DIR_NAME = ".codegen"
CONFIG_FILENAME = "config.toml"
CONFIG_PATH = CODEGEN_REPO_ROOT / CODEGEN_DIR_NAME / CONFIG_FILENAME

# Environment variables
ENV_FILENAME = ".env"
ENV_PATH = CODEGEN_REPO_ROOT / "src" / "codegen" / ENV_FILENAME
130 changes: 130 additions & 0 deletions src/codegen/shared/configs/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import json
from pathlib import Path

import toml
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

from codegen.shared.configs.constants import CONFIG_PATH, ENV_PATH


class TypescriptConfig(BaseModel):
ts_dependency_manager: bool = False
ts_language_engine: bool = False
v8_ts_engine: bool = False


class CodebaseFeatureFlags(BaseModel):
debug: bool = False
verify_graph: bool = False
track_graph: bool = False
method_usages: bool = True
sync_enabled: bool = True
full_range_index: bool = False
ignore_process_errors: bool = True
disable_graph: bool = False
generics: bool = True
import_resolution_overrides: dict[str, str] = Field(default_factory=lambda: {})
typescript: TypescriptConfig = Field(default_factory=TypescriptConfig)


class RepositoryConfig(BaseModel):
organization_name: str | None = None
repo_name: str | None = None


class SecretsConfig(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="CODEGEN_SECRETS__",
env_file=ENV_PATH,
case_sensitive=False,
)
github_token: str | None = None
openai_api_key: str | None = None


class FeatureFlagsConfig(BaseModel):
codebase: CodebaseFeatureFlags = Field(default_factory=CodebaseFeatureFlags)


class Config(BaseSettings):
model_config = SettingsConfigDict(

Check failure on line 51 in src/codegen/shared/configs/models.py

View workflow job for this annotation

GitHub Actions / mypy

error: Extra key "exclude_defaults" for TypedDict "SettingsConfigDict" [typeddict-unknown-key]
extra="ignore",
exclude_defaults=False,
)
secrets: SecretsConfig = Field(default_factory=SecretsConfig)
repository: RepositoryConfig = Field(default_factory=RepositoryConfig)
feature_flags: FeatureFlagsConfig = Field(default_factory=FeatureFlagsConfig)

def save(self, config_path: Path | None = None) -> None:
"""Save configuration to the config file."""
path = config_path or CONFIG_PATH

path.parent.mkdir(parents=True, exist_ok=True)

with open(path, "w") as f:
toml.dump(self.model_dump(exclude_none=True), f)

def get(self, full_key: str) -> str | None:
"""Get a configuration value as a JSON string."""
data = self.model_dump()
keys = full_key.split(".")
current = data
for k in keys:
if not isinstance(current, dict) or k not in current:
return None
current = current[k]
return json.dumps(current)

def set(self, full_key: str, value: str) -> None:
"""Update a configuration value and save it to the config file.

Args:
full_key: Dot-separated path to the config value (e.g. "feature_flags.codebase.debug")
value: string representing the new value
"""
data = self.model_dump()
keys = full_key.split(".")
current = data
current_attr = self

# Traverse through the key path and validate
for k in keys[:-1]:
if not isinstance(current, dict) or k not in current:
msg = f"Invalid configuration path: {full_key}"
raise KeyError(msg)
current = current[k]
current_attr = current_attr.__getattribute__(k)

if not isinstance(current, dict) or keys[-1] not in current:
msg = f"Invalid configuration path: {full_key}"
raise KeyError(msg)

# Validate the value type at key
field_info = current_attr.model_fields[keys[-1]].annotation
if isinstance(field_info, BaseModel):
try:
Config.model_validate(value, strict=False)
except Exception as e:
msg = f"Value does not match the expected type for key: {full_key}\n\nError:{e}"
raise ValueError(msg)

# Set the key value
if isinstance(current[keys[-1]], dict):
try:
current[keys[-1]] = json.loads(value)
except json.JSONDecodeError as e:
msg = f"Value must be a valid JSON object for key: {full_key}\n\nError:{e}"
raise ValueError(msg)
else:
current[keys[-1]] = value

# Update the Config object with the new data
self.__dict__.update(self.__class__.model_validate(data).__dict__)

# Save to config file
self.save()

def __str__(self) -> str:
"""Return a pretty-printed string representation of the config."""
return json.dumps(self.model_dump(exclude_none=False), indent=2)
15 changes: 15 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading