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
23 changes: 23 additions & 0 deletions .codegen/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
[secrets]
github_token = ""
openai_api_key = ""

[repository]
organization_name = "codegen-sh"
repo_name = "codegen-sdk"

[feature_flags.codebase]
debug = false
verify_graph = false
track_graph = false
method_usages = true
sync_enabled = true
full_range_index = false
ignore_process_errors = true
disable_graph = false
generics = true

[feature_flags.codebase.import_resolution_overrides]

[feature_flags.codebase.typescript]
ts_dependency_manager = false
ts_language_engine = false
v8_ts_engine = false
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 = "team+codegenbot@codegen.sh"
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 | None = None
ts_language_engine: bool | None = None
v8_ts_engine: bool | None = None


class CodebaseFeatureFlags(BaseModel):
debug: bool | None = None
verify_graph: bool | None = None
track_graph: bool | None = None
method_usages: bool | None = None
sync_enabled: bool | None = None
full_range_index: bool | None = None
ignore_process_errors: bool | None = None
disable_graph: bool | None = None
generics: bool | None = None
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)

Check warning on line 101 in src/codegen/shared/configs/models.py

View check run for this annotation

Codecov / codecov/patch

src/codegen/shared/configs/models.py#L100-L101

Added lines #L100 - L101 were not covered by tests

# 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)

Check warning on line 110 in src/codegen/shared/configs/models.py

View check run for this annotation

Codecov / codecov/patch

src/codegen/shared/configs/models.py#L106-L110

Added lines #L106 - L110 were not covered by tests

# 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)
File renamed without changes.
Loading
Loading