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
100 changes: 100 additions & 0 deletions navconfig/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Command line tools for managing NavConfig projects."""
from __future__ import annotations

import argparse
import logging
from pathlib import Path
from textwrap import dedent

LOGGER = logging.getLogger(__name__)


DEFAULT_ENV_CONTENT = """\
# Generated by NavConfig
# Update these values to match your environment specifics.
ENV={environment}
DEBUG=true
CONFIG_FILE=etc/config.ini
"""

DEFAULT_CONFIG_CONTENT = """\
[navconfig]
# Default configuration generated by NavConfig.
# Adjust values to match your project requirements.
DEBUG = true
CONFIG_FILE = etc/config.ini
"""


def create_project_structure(env_name: str, project_root: Path) -> dict[str, Path]:
"""Create the default NavConfig project structure."""
env_directory = project_root / "env" / env_name
env_directory.mkdir(parents=True, exist_ok=True)

env_file = env_directory / ".env"
if not env_file.exists():
env_file.write_text(DEFAULT_ENV_CONTENT.format(environment=env_name), encoding="utf-8")

etc_directory = project_root / "etc"
etc_directory.mkdir(parents=True, exist_ok=True)

config_file = etc_directory / "config.ini"
if not config_file.exists():
config_file.write_text(dedent(DEFAULT_CONFIG_CONTENT), encoding="utf-8")

return {
"env_directory": env_directory,
"env_file": env_file,
"etc_directory": etc_directory,
"config_file": config_file,
}


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="kardex",
description="Utilities for bootstrapping NavConfig projects.",
)
subparsers = parser.add_subparsers(dest="command", required=True)

create_parser = subparsers.add_parser(
"create",
help="Create the default NavConfig environment structure.",
)
create_parser.add_argument(
"--env",
default="dev",
help="Name of the environment to create (default: dev).",
)
create_parser.add_argument(
"--path",
default=".",
help="Project directory where the structure should be created.",
)

return parser


def main(argv: list[str] | None = None) -> int:
"""Entry point for the kardex CLI."""
parser = build_parser()
args = parser.parse_args(argv)

if args.command == "create":
project_root = Path(args.path).resolve()
created_paths = create_project_structure(args.env, project_root)
LOGGER.info(
"Created NavConfig project structure at %s (environment: %s)",
project_root,
args.env,
)
for name, path in created_paths.items():
LOGGER.debug("%s -> %s", name, path)
return 0

parser.print_help()
return 1


if __name__ == "__main__":
raise SystemExit(main())
12 changes: 10 additions & 2 deletions navconfig/kardex.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,16 @@ def load_environment(self, env_type: str = "vault", override: bool = False):
if self._mapping_ is None:
self._mapping_ = {} # empty dict
except (FileExistsError, FileNotFoundError) as ex:
logging.warning(str(ex))
raise
error_message = (
"NavConfig initialization failed: environment assets are missing.\n"
f"Original error: {ex}\n"
"Ensure your project contains an 'env' directory with the selected "
"environment subfolder and a '.env' file (e.g. env/"
f"{self.ENV or 'dev'}/.env).\n"
"You can scaffold the required files by running `kardex create`."
)
logging.warning(error_message)
raise type(ex)(error_message) from ex
except RuntimeError as ex:
raise RuntimeError(str(ex)) from ex
except Exception as ex:
Expand Down
6 changes: 5 additions & 1 deletion navconfig/loaders/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ def __init__(
for warning in warnings:
logging.warning(f"NavConfig: {warning}")
raise FileExistsError(
f"{type(self).__name__}: No Directory Path: {env_path}"
"NavConfig could not find the expected environment directory. "
f"Looked for: {env_path}.\n"
"NavConfig projects require an 'env/<environment>/.env' file. "
"Run `kardex create` to scaffold the default structure (env folder, "
"base .env file, and etc/config.ini)."
)

@abstractmethod
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ dependencies = [
]
dynamic = ["version"]

[project.scripts]
kardex = "navconfig.cli:main"

[project.urls]
Homepage = "https://github.com/phenobarbital/navconfig"
Repository = "https://github.com/phenobarbital/navconfig"
Expand Down
21 changes: 21 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,27 @@ async def test_environment_switching_idempotent():
assert config.get_current_env() == current_env


async def test_kardex_cli_create(tmp_path, caplog):
"""Ensure the kardex CLI scaffolds the expected project structure."""
from navconfig.cli import main

caplog.set_level(logging.INFO)
exit_code = main(["create", "--env", "qa", "--path", str(tmp_path)])

assert exit_code == 0

env_file = tmp_path / "env" / "qa" / ".env"
config_file = tmp_path / "etc" / "config.ini"

assert env_file.exists()
assert config_file.exists()

assert "ENV=qa" in env_file.read_text()
assert "[navconfig]" in config_file.read_text()

assert any("Created NavConfig project structure" in message for message in caplog.messages)


async def test_cross_environment_query():
"""Test querying variables from different environments without switching."""
from navconfig import config
Expand Down