Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92f42ffce3 | |||
| 58408d0dbe | |||
| 2f1ac3c758 | |||
| d3b69f7459 | |||
| 50c8204fa0 | |||
| 507fe0f40e | |||
| b7b40eada1 | |||
| 9765386405 |
+4
-4
@@ -27,7 +27,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
|||||||
pip install $RUNNER
|
pip install $RUNNER
|
||||||
|
|
||||||
# Set working directory for dependency installation
|
# Set working directory for dependency installation
|
||||||
WORKDIR /tmp
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy dependency files
|
# Copy dependency files
|
||||||
COPY pyproject.toml poetry.lock* uv.lock* Makefile ./
|
COPY pyproject.toml poetry.lock* uv.lock* Makefile ./
|
||||||
@@ -43,8 +43,10 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
|||||||
uv pip install --system -r pyproject.toml; \
|
uv pip install --system -r pyproject.toml; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
COPY alfred/ ./alfred/
|
||||||
COPY scripts/ ./scripts/
|
COPY scripts/ ./scripts/
|
||||||
COPY .env.example ./
|
COPY .env.example ./
|
||||||
|
COPY settings.toml ./
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Stage 2: Testing
|
# Stage 2: Testing
|
||||||
@@ -62,8 +64,6 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
|||||||
uv pip install --system -e .[dev]; \
|
uv pip install --system -e .[dev]; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
COPY alfred/ ./alfred
|
|
||||||
COPY scripts ./scripts
|
|
||||||
COPY tests/ ./tests
|
COPY tests/ ./tests
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -120,4 +120,4 @@ EXPOSE 8000
|
|||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5).raise_for_status()" || exit 1
|
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5).raise_for_status()" || exit 1
|
||||||
|
|
||||||
CMD ["python", "-m", "uvicorn", "alfred.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["python", "-m", "uvicorn", "alfred.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|||||||
+3
-3
@@ -41,10 +41,10 @@ def _get_secret_factory(rule: str):
|
|||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
"""
|
"""
|
||||||
Application settings.
|
Application settings management.
|
||||||
|
|
||||||
Settings are loaded from .env and validated using the schema
|
Initializes configuration with internal defaults from the settings module,
|
||||||
defined in pyproject.toml.
|
then overrides them with environment variables loaded from a .env file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
|
|||||||
@@ -185,39 +185,26 @@ def load_schema(base_dir: Path | None = None) -> SettingsSchema:
|
|||||||
if base_dir is None:
|
if base_dir is None:
|
||||||
base_dir = BASE_DIR
|
base_dir = BASE_DIR
|
||||||
|
|
||||||
# Try settings.toml first (cleaner, dedicated file)
|
# Load from settings.toml (required)
|
||||||
settings_toml_path = base_dir / "settings.toml"
|
settings_toml_path = base_dir / "settings.toml"
|
||||||
if settings_toml_path.exists():
|
|
||||||
with open(settings_toml_path, "rb") as f:
|
|
||||||
data = tomllib.load(f)
|
|
||||||
|
|
||||||
try:
|
if not settings_toml_path.exists():
|
||||||
schema_dict = data["tool"]["alfred"]["settings_schema"]
|
|
||||||
return SettingsSchema(schema_dict)
|
|
||||||
except KeyError as e:
|
|
||||||
raise KeyError(
|
|
||||||
"Missing [tool.alfred.settings_schema] section in settings.toml"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
# Fallback to pyproject.toml
|
|
||||||
toml_path = base_dir / "pyproject.toml"
|
|
||||||
if not toml_path.exists():
|
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"Neither settings.toml nor pyproject.toml found in {base_dir}"
|
f"settings.toml not found at {settings_toml_path}. "
|
||||||
|
f"This file is required and must be present in the application root."
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(toml_path, "rb") as f:
|
with open(settings_toml_path, "rb") as f:
|
||||||
data = tomllib.load(f)
|
data = tomllib.load(f)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
schema_dict = data["tool"]["alfred"]["settings_schema"]
|
schema_dict = data["tool"]["alfred"]["settings_schema"]
|
||||||
|
return SettingsSchema(schema_dict)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise KeyError(
|
raise KeyError(
|
||||||
"Missing [tool.alfred.settings_schema] section in pyproject.toml"
|
f"Missing [tool.alfred.settings_schema] section in settings.toml at {settings_toml_path}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
return SettingsSchema(schema_dict)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_value(definition: SettingDefinition, value: Any) -> bool:
|
def validate_value(definition: SettingDefinition, value: Any) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+3
-2
@@ -17,6 +17,7 @@ services:
|
|||||||
# --- MAIN APPLICATION ---
|
# --- MAIN APPLICATION ---
|
||||||
alfred:
|
alfred:
|
||||||
container_name: alfred-core
|
container_name: alfred-core
|
||||||
|
image: alfred_media_organizer:latest
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
@@ -34,7 +35,7 @@ services:
|
|||||||
- ./data:/data
|
- ./data:/data
|
||||||
- ./logs:/logs
|
- ./logs:/logs
|
||||||
# TODO: Hot reload (comment out in production)
|
# TODO: Hot reload (comment out in production)
|
||||||
- ./alfred:/home/appuser/alfred
|
#- ./alfred:/home/appuser/alfred
|
||||||
command: >
|
command: >
|
||||||
sh -c "python -u -m uvicorn alfred.app:app --host 0.0.0.0 --port 8000 2>&1 | tee -a /logs/alfred.log"
|
sh -c "python -u -m uvicorn alfred.app:app --host 0.0.0.0 --port 8000 2>&1 | tee -a /logs/alfred.log"
|
||||||
networks:
|
networks:
|
||||||
@@ -169,7 +170,7 @@ services:
|
|||||||
- ./data/vectordb:/var/lib/postgresql/data
|
- ./data/vectordb:/var/lib/postgresql/data
|
||||||
profiles: ["rag", "full"]
|
profiles: ["rag", "full"]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-alfred} -d $${POSTGRES_DB_NAME:-alfred}" ]
|
test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB_NAME}" ]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
Generated
+6
-6
@@ -74,13 +74,13 @@ wcmatch = ">=8.5.1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2025.11.12"
|
version = "2026.1.4"
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
|
{file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"},
|
||||||
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
|
{file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -394,13 +394,13 @@ standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[stand
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.20.1"
|
version = "3.20.2"
|
||||||
description = "A platform independent file lock."
|
description = "A platform independent file lock."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
files = [
|
files = [
|
||||||
{file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"},
|
{file = "filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8"},
|
||||||
{file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"},
|
{file = "filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ from alfred.settings_bootstrap import (
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_toml_content():
|
def test_pyproject_content():
|
||||||
"""Test TOML content with schema."""
|
"""Test pyproject.toml content with poetry metadata."""
|
||||||
return """
|
return """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
@@ -25,7 +25,13 @@ python = "==3.14.2"
|
|||||||
[tool.alfred.settings]
|
[tool.alfred.settings]
|
||||||
runner = "poetry"
|
runner = "poetry"
|
||||||
image_name = "test_image"
|
image_name = "test_image"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_settings_content():
|
||||||
|
"""Test settings.toml content with schema."""
|
||||||
|
return """
|
||||||
[tool.alfred.settings_schema.TEST_FROM_TOML]
|
[tool.alfred.settings_schema.TEST_FROM_TOML]
|
||||||
type = "string"
|
type = "string"
|
||||||
source = "toml"
|
source = "toml"
|
||||||
@@ -72,11 +78,15 @@ def config_source(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def create_test_env(tmp_path, test_toml_content):
|
def create_test_env(tmp_path, test_pyproject_content, test_settings_content):
|
||||||
"""Create a complete test environment."""
|
"""Create a complete test environment."""
|
||||||
# Create pyproject.toml
|
# Create pyproject.toml
|
||||||
toml_path = tmp_path / "pyproject.toml"
|
pyproject_path = tmp_path / "pyproject.toml"
|
||||||
toml_path.write_text(test_toml_content)
|
pyproject_path.write_text(test_pyproject_content)
|
||||||
|
|
||||||
|
# Create settings.toml
|
||||||
|
settings_path = tmp_path / "settings.toml"
|
||||||
|
settings_path.write_text(test_settings_content)
|
||||||
|
|
||||||
# Create .env.example
|
# Create .env.example
|
||||||
env_example = tmp_path / ".env.example"
|
env_example = tmp_path / ".env.example"
|
||||||
@@ -330,11 +340,17 @@ TEST_SECRET=my_secret_123
|
|||||||
env_content = create_test_env.env_path.read_text()
|
env_content = create_test_env.env_path.read_text()
|
||||||
assert "TEST_SECRET=my_secret_123" in env_content
|
assert "TEST_SECRET=my_secret_123" in env_content
|
||||||
|
|
||||||
def test_validation_error(self, tmp_path, test_toml_content):
|
def test_validation_error(
|
||||||
|
self, tmp_path, test_pyproject_content, test_settings_content
|
||||||
|
):
|
||||||
"""Test validation error is raised."""
|
"""Test validation error is raised."""
|
||||||
# Add a setting with validation
|
# Create pyproject.toml
|
||||||
toml_with_validation = (
|
pyproject_path = tmp_path / "pyproject.toml"
|
||||||
test_toml_content
|
pyproject_path.write_text(test_pyproject_content)
|
||||||
|
|
||||||
|
# Add a setting with validation to settings.toml
|
||||||
|
settings_with_validation = (
|
||||||
|
test_settings_content
|
||||||
+ """
|
+ """
|
||||||
[tool.alfred.settings_schema.TEST_VALIDATED]
|
[tool.alfred.settings_schema.TEST_VALIDATED]
|
||||||
type = "integer"
|
type = "integer"
|
||||||
@@ -345,8 +361,8 @@ description = "Validated setting"
|
|||||||
category = "test"
|
category = "test"
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
toml_path = tmp_path / "pyproject.toml"
|
settings_path = tmp_path / "settings.toml"
|
||||||
toml_path.write_text(toml_with_validation)
|
settings_path.write_text(settings_with_validation)
|
||||||
|
|
||||||
env_example = tmp_path / ".env.example"
|
env_example = tmp_path / ".env.example"
|
||||||
env_example.write_text("TEST_VALIDATED=\n")
|
env_example.write_text("TEST_VALIDATED=\n")
|
||||||
@@ -364,9 +380,10 @@ category = "test"
|
|||||||
|
|
||||||
def test_write_env_make_only_exports(self, create_test_env):
|
def test_write_env_make_only_exports(self, create_test_env):
|
||||||
"""Test that .env.make only contains export_to_env_make settings."""
|
"""Test that .env.make only contains export_to_env_make settings."""
|
||||||
# Add a setting with export_to_env_make
|
# Add a setting with export_to_env_make to settings.toml
|
||||||
toml_content = create_test_env.toml_path.read_text()
|
settings_path = create_test_env.base_dir / "settings.toml"
|
||||||
toml_content += """
|
settings_content = settings_path.read_text()
|
||||||
|
settings_content += """
|
||||||
[tool.alfred.settings_schema.EXPORTED_VAR]
|
[tool.alfred.settings_schema.EXPORTED_VAR]
|
||||||
type = "string"
|
type = "string"
|
||||||
source = "env"
|
source = "env"
|
||||||
@@ -374,7 +391,7 @@ default = "exported"
|
|||||||
export_to_env_make = true
|
export_to_env_make = true
|
||||||
category = "build"
|
category = "build"
|
||||||
"""
|
"""
|
||||||
create_test_env.toml_path.write_text(toml_content)
|
settings_path.write_text(settings_content)
|
||||||
|
|
||||||
# Recreate schema
|
# Recreate schema
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|||||||
@@ -8,14 +8,19 @@ from alfred.settings_bootstrap import ConfigSource, SettingsBootstrap
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_toml_with_all_types(tmp_path):
|
def test_toml_with_all_types(tmp_path):
|
||||||
"""Create test TOML with all setting types."""
|
"""Create test TOML with all setting types."""
|
||||||
toml_content = """
|
# Create pyproject.toml for poetry metadata
|
||||||
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.14"
|
python = "^3.14"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
# Create settings.toml with schema
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.STRING_VAR]
|
[tool.alfred.settings_schema.STRING_VAR]
|
||||||
type = "string"
|
type = "string"
|
||||||
source = "env"
|
source = "env"
|
||||||
@@ -59,14 +64,9 @@ compute_template = "{STRING_VAR}_{INT_VAR}"
|
|||||||
description = "Computed variable"
|
description = "Computed variable"
|
||||||
category = "test"
|
category = "test"
|
||||||
"""
|
"""
|
||||||
toml_path = tmp_path / "pyproject.toml"
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
toml_path.write_text(toml_content)
|
|
||||||
return tmp_path
|
|
||||||
|
|
||||||
|
# Create .env.example
|
||||||
@pytest.fixture
|
|
||||||
def test_env_example(tmp_path):
|
|
||||||
"""Create .env.example template."""
|
|
||||||
env_example_content = """# Test configuration
|
env_example_content = """# Test configuration
|
||||||
STRING_VAR=
|
STRING_VAR=
|
||||||
INT_VAR=
|
INT_VAR=
|
||||||
@@ -81,18 +81,18 @@ SECRET_VAR=
|
|||||||
# Computed values
|
# Computed values
|
||||||
COMPUTED_VAR=
|
COMPUTED_VAR=
|
||||||
|
|
||||||
# Custom section
|
# Custom variable (not in schema)
|
||||||
CUSTOM_VAR=custom_value
|
CUSTOM_VAR=custom_value
|
||||||
"""
|
"""
|
||||||
env_example_path = tmp_path / ".env.example"
|
(tmp_path / ".env.example").write_text(env_example_content)
|
||||||
env_example_path.write_text(env_example_content)
|
|
||||||
return env_example_path
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
class TestTemplatePreservation:
|
class TestTemplatePreservation:
|
||||||
"""Test that .env.example template structure is preserved."""
|
"""Test that .env.example template structure is preserved."""
|
||||||
|
|
||||||
def test_preserves_comments(self, test_toml_with_all_types, test_env_example):
|
def test_preserves_comments(self, test_toml_with_all_types):
|
||||||
"""Test that comments from .env.example are preserved."""
|
"""Test that comments from .env.example are preserved."""
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ class TestTemplatePreservation:
|
|||||||
assert "# Security" in env_content
|
assert "# Security" in env_content
|
||||||
assert "# Computed values" in env_content
|
assert "# Computed values" in env_content
|
||||||
|
|
||||||
def test_preserves_empty_lines(self, test_toml_with_all_types, test_env_example):
|
def test_preserves_empty_lines(self, test_toml_with_all_types):
|
||||||
"""Test that empty lines from .env.example are preserved."""
|
"""Test that empty lines from .env.example are preserved."""
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ class TestTemplatePreservation:
|
|||||||
# Check there are empty lines (structure preserved)
|
# Check there are empty lines (structure preserved)
|
||||||
assert "" in lines
|
assert "" in lines
|
||||||
|
|
||||||
def test_preserves_variable_order(self, test_toml_with_all_types, test_env_example):
|
def test_preserves_variable_order(self, test_toml_with_all_types):
|
||||||
"""Test that variable order from .env.example is preserved."""
|
"""Test that variable order from .env.example is preserved."""
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -150,9 +150,7 @@ class TestTemplatePreservation:
|
|||||||
class TestSecretPreservation:
|
class TestSecretPreservation:
|
||||||
"""Test that secrets are never overwritten."""
|
"""Test that secrets are never overwritten."""
|
||||||
|
|
||||||
def test_preserves_existing_secrets(
|
def test_preserves_existing_secrets(self, test_toml_with_all_types):
|
||||||
self, test_toml_with_all_types, test_env_example
|
|
||||||
):
|
|
||||||
"""Test that existing secrets are preserved across multiple bootstraps."""
|
"""Test that existing secrets are preserved across multiple bootstraps."""
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -186,11 +184,14 @@ class TestSecretPreservation:
|
|||||||
|
|
||||||
def test_multiple_secrets_preserved(self, tmp_path):
|
def test_multiple_secrets_preserved(self, tmp_path):
|
||||||
"""Test that multiple secrets are all preserved."""
|
"""Test that multiple secrets are all preserved."""
|
||||||
toml_content = """
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.SECRET_1]
|
[tool.alfred.settings_schema.SECRET_1]
|
||||||
type = "secret"
|
type = "secret"
|
||||||
source = "generated"
|
source = "generated"
|
||||||
@@ -209,7 +210,7 @@ source = "generated"
|
|||||||
secret_rule = "8:hex"
|
secret_rule = "8:hex"
|
||||||
category = "security"
|
category = "security"
|
||||||
"""
|
"""
|
||||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
(tmp_path / ".env.example").write_text("SECRET_1=\nSECRET_2=\nSECRET_3=\n")
|
(tmp_path / ".env.example").write_text("SECRET_1=\nSECRET_2=\nSECRET_3=\n")
|
||||||
|
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
@@ -236,9 +237,7 @@ category = "security"
|
|||||||
class TestCustomVariables:
|
class TestCustomVariables:
|
||||||
"""Test that custom variables (not in schema) are preserved."""
|
"""Test that custom variables (not in schema) are preserved."""
|
||||||
|
|
||||||
def test_preserves_custom_variables_from_env(
|
def test_preserves_custom_variables_from_env(self, test_toml_with_all_types):
|
||||||
self, test_toml_with_all_types, test_env_example
|
|
||||||
):
|
|
||||||
"""Test that custom variables added to .env are preserved."""
|
"""Test that custom variables added to .env are preserved."""
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -264,9 +263,7 @@ class TestCustomVariables:
|
|||||||
assert "MY_CUSTOM_VAR=custom_value" in env_content
|
assert "MY_CUSTOM_VAR=custom_value" in env_content
|
||||||
assert "ANOTHER_CUSTOM=another_value" in env_content
|
assert "ANOTHER_CUSTOM=another_value" in env_content
|
||||||
|
|
||||||
def test_custom_variables_in_dedicated_section(
|
def test_custom_variables_in_dedicated_section(self, test_toml_with_all_types):
|
||||||
self, test_toml_with_all_types, test_env_example
|
|
||||||
):
|
|
||||||
"""Test that custom variables are placed in a dedicated section."""
|
"""Test that custom variables are placed in a dedicated section."""
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -285,9 +282,7 @@ class TestCustomVariables:
|
|||||||
assert "# --- CUSTOM VARIABLES ---" in env_content
|
assert "# --- CUSTOM VARIABLES ---" in env_content
|
||||||
assert "MY_CUSTOM_VAR=test" in env_content
|
assert "MY_CUSTOM_VAR=test" in env_content
|
||||||
|
|
||||||
def test_preserves_custom_from_env_example(
|
def test_preserves_custom_from_env_example(self, test_toml_with_all_types):
|
||||||
self, test_toml_with_all_types, test_env_example
|
|
||||||
):
|
|
||||||
"""Test that custom variables in .env.example are preserved."""
|
"""Test that custom variables in .env.example are preserved."""
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -306,9 +301,7 @@ class TestCustomVariables:
|
|||||||
class TestBooleanHandling:
|
class TestBooleanHandling:
|
||||||
"""Test that booleans are handled correctly."""
|
"""Test that booleans are handled correctly."""
|
||||||
|
|
||||||
def test_booleans_written_as_lowercase(
|
def test_booleans_written_as_lowercase(self, test_toml_with_all_types):
|
||||||
self, test_toml_with_all_types, test_env_example
|
|
||||||
):
|
|
||||||
"""Test that Python booleans are written as lowercase strings."""
|
"""Test that Python booleans are written as lowercase strings."""
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -327,18 +320,21 @@ class TestBooleanHandling:
|
|||||||
|
|
||||||
def test_false_boolean_written_as_lowercase(self, tmp_path):
|
def test_false_boolean_written_as_lowercase(self, tmp_path):
|
||||||
"""Test that False is written as 'false'."""
|
"""Test that False is written as 'false'."""
|
||||||
toml_content = """
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.BOOL_FALSE]
|
[tool.alfred.settings_schema.BOOL_FALSE]
|
||||||
type = "boolean"
|
type = "boolean"
|
||||||
source = "env"
|
source = "env"
|
||||||
default = false
|
default = false
|
||||||
category = "test"
|
category = "test"
|
||||||
"""
|
"""
|
||||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
(tmp_path / ".env.example").write_text("BOOL_FALSE=\n")
|
(tmp_path / ".env.example").write_text("BOOL_FALSE=\n")
|
||||||
|
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
@@ -356,18 +352,21 @@ category = "test"
|
|||||||
|
|
||||||
def test_boolean_parsing_from_env(self, tmp_path):
|
def test_boolean_parsing_from_env(self, tmp_path):
|
||||||
"""Test that various boolean formats are parsed correctly."""
|
"""Test that various boolean formats are parsed correctly."""
|
||||||
toml_content = """
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.BOOL_VAR]
|
[tool.alfred.settings_schema.BOOL_VAR]
|
||||||
type = "boolean"
|
type = "boolean"
|
||||||
source = "env"
|
source = "env"
|
||||||
default = false
|
default = false
|
||||||
category = "test"
|
category = "test"
|
||||||
"""
|
"""
|
||||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
(tmp_path / ".env.example").write_text("BOOL_VAR=\n")
|
(tmp_path / ".env.example").write_text("BOOL_VAR=\n")
|
||||||
|
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
@@ -402,9 +401,7 @@ category = "test"
|
|||||||
class TestComputedVariables:
|
class TestComputedVariables:
|
||||||
"""Test that computed variables are calculated correctly."""
|
"""Test that computed variables are calculated correctly."""
|
||||||
|
|
||||||
def test_computed_variables_written_to_env(
|
def test_computed_variables_written_to_env(self, test_toml_with_all_types):
|
||||||
self, test_toml_with_all_types, test_env_example
|
|
||||||
):
|
|
||||||
"""Test that computed variables are written with their computed values."""
|
"""Test that computed variables are written with their computed values."""
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -421,11 +418,14 @@ class TestComputedVariables:
|
|||||||
|
|
||||||
def test_computed_uri_example(self, tmp_path):
|
def test_computed_uri_example(self, tmp_path):
|
||||||
"""Test computed URI (like MONGO_URI) is written correctly."""
|
"""Test computed URI (like MONGO_URI) is written correctly."""
|
||||||
toml_content = """
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.DB_HOST]
|
[tool.alfred.settings_schema.DB_HOST]
|
||||||
type = "string"
|
type = "string"
|
||||||
source = "env"
|
source = "env"
|
||||||
@@ -457,7 +457,7 @@ compute_from = ["DB_USER", "DB_PASSWORD", "DB_HOST", "DB_PORT"]
|
|||||||
compute_template = "postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/db"
|
compute_template = "postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/db"
|
||||||
category = "database"
|
category = "database"
|
||||||
"""
|
"""
|
||||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
(tmp_path / ".env.example").write_text(
|
(tmp_path / ".env.example").write_text(
|
||||||
"DB_HOST=\nDB_PORT=\nDB_USER=\nDB_PASSWORD=\nDB_URI=\n"
|
"DB_HOST=\nDB_PORT=\nDB_USER=\nDB_PASSWORD=\nDB_URI=\n"
|
||||||
)
|
)
|
||||||
@@ -491,18 +491,21 @@ class TestEdgeCases:
|
|||||||
|
|
||||||
def test_missing_env_example(self, tmp_path):
|
def test_missing_env_example(self, tmp_path):
|
||||||
"""Test that missing .env.example raises error."""
|
"""Test that missing .env.example raises error."""
|
||||||
toml_content = """
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.TEST_VAR]
|
[tool.alfred.settings_schema.TEST_VAR]
|
||||||
type = "string"
|
type = "string"
|
||||||
source = "env"
|
source = "env"
|
||||||
default = "test"
|
default = "test"
|
||||||
category = "test"
|
category = "test"
|
||||||
"""
|
"""
|
||||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
|
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
|
|
||||||
@@ -516,18 +519,21 @@ category = "test"
|
|||||||
|
|
||||||
def test_empty_env_example(self, tmp_path):
|
def test_empty_env_example(self, tmp_path):
|
||||||
"""Test that empty .env.example works."""
|
"""Test that empty .env.example works."""
|
||||||
toml_content = """
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.TEST_VAR]
|
[tool.alfred.settings_schema.TEST_VAR]
|
||||||
type = "string"
|
type = "string"
|
||||||
source = "env"
|
source = "env"
|
||||||
default = "test"
|
default = "test"
|
||||||
category = "test"
|
category = "test"
|
||||||
"""
|
"""
|
||||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
(tmp_path / ".env.example").write_text("")
|
(tmp_path / ".env.example").write_text("")
|
||||||
|
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
@@ -543,18 +549,21 @@ category = "test"
|
|||||||
|
|
||||||
def test_variable_with_equals_in_value(self, tmp_path):
|
def test_variable_with_equals_in_value(self, tmp_path):
|
||||||
"""Test that variables with '=' in their value are handled correctly."""
|
"""Test that variables with '=' in their value are handled correctly."""
|
||||||
toml_content = """
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.URL_VAR]
|
[tool.alfred.settings_schema.URL_VAR]
|
||||||
type = "string"
|
type = "string"
|
||||||
source = "env"
|
source = "env"
|
||||||
default = "http://example.com?key=value"
|
default = "http://example.com?key=value"
|
||||||
category = "test"
|
category = "test"
|
||||||
"""
|
"""
|
||||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
(tmp_path / ".env.example").write_text("URL_VAR=\n")
|
(tmp_path / ".env.example").write_text("URL_VAR=\n")
|
||||||
|
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
@@ -571,11 +580,14 @@ category = "test"
|
|||||||
|
|
||||||
def test_preserves_existing_values_on_update(self, tmp_path):
|
def test_preserves_existing_values_on_update(self, tmp_path):
|
||||||
"""Test that existing values are preserved when updating."""
|
"""Test that existing values are preserved when updating."""
|
||||||
toml_content = """
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.VAR1]
|
[tool.alfred.settings_schema.VAR1]
|
||||||
type = "string"
|
type = "string"
|
||||||
source = "env"
|
source = "env"
|
||||||
@@ -588,7 +600,7 @@ source = "env"
|
|||||||
default = "default2"
|
default = "default2"
|
||||||
category = "test"
|
category = "test"
|
||||||
"""
|
"""
|
||||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
(tmp_path / ".env.example").write_text("VAR1=\nVAR2=\n")
|
(tmp_path / ".env.example").write_text("VAR1=\nVAR2=\n")
|
||||||
|
|
||||||
from alfred.settings_schema import load_schema
|
from alfred.settings_schema import load_schema
|
||||||
@@ -619,14 +631,17 @@ class TestIntegration:
|
|||||||
|
|
||||||
def test_full_workflow_like_alfred(self, tmp_path):
|
def test_full_workflow_like_alfred(self, tmp_path):
|
||||||
"""Test a full workflow similar to Alfred's actual usage."""
|
"""Test a full workflow similar to Alfred's actual usage."""
|
||||||
toml_content = """
|
pyproject_content = """
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "alfred"
|
name = "alfred"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.14"
|
python = "^3.14"
|
||||||
|
"""
|
||||||
|
(tmp_path / "pyproject.toml").write_text(pyproject_content)
|
||||||
|
|
||||||
|
settings_content = """
|
||||||
[tool.alfred.settings_schema.ALFRED_VERSION]
|
[tool.alfred.settings_schema.ALFRED_VERSION]
|
||||||
type = "string"
|
type = "string"
|
||||||
source = "toml"
|
source = "toml"
|
||||||
@@ -677,7 +692,7 @@ source = "env"
|
|||||||
default = false
|
default = false
|
||||||
category = "app"
|
category = "app"
|
||||||
"""
|
"""
|
||||||
(tmp_path / "pyproject.toml").write_text(toml_content)
|
(tmp_path / "settings.toml").write_text(settings_content)
|
||||||
|
|
||||||
env_example_content = """# Application settings
|
env_example_content = """# Application settings
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
@@ -715,7 +730,12 @@ ALFRED_VERSION=
|
|||||||
assert "DEBUG_MODE=false" in env_content_1 # lowercase!
|
assert "DEBUG_MODE=false" in env_content_1 # lowercase!
|
||||||
assert "ALFRED_VERSION=0.1.7" in env_content_1
|
assert "ALFRED_VERSION=0.1.7" in env_content_1
|
||||||
assert "JWT_SECRET=" in env_content_1
|
assert "JWT_SECRET=" in env_content_1
|
||||||
assert len([l for l in env_content_1.split("\n") if "JWT_SECRET=" in l][0]) > 20
|
assert (
|
||||||
|
len(
|
||||||
|
[line for line in env_content_1.split("\n") if "JWT_SECRET=" in line][0]
|
||||||
|
)
|
||||||
|
> 20
|
||||||
|
)
|
||||||
assert "MONGO_URI=mongodb://user:" in env_content_1
|
assert "MONGO_URI=mongodb://user:" in env_content_1
|
||||||
|
|
||||||
# Second bootstrap - should preserve everything
|
# Second bootstrap - should preserve everything
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def create_schema_file(tmp_path):
|
|||||||
"""Factory to create pyproject.toml with schema."""
|
"""Factory to create pyproject.toml with schema."""
|
||||||
|
|
||||||
def _create(content: str):
|
def _create(content: str):
|
||||||
toml_path = tmp_path / "pyproject.toml"
|
toml_path = tmp_path / "settings.toml"
|
||||||
full_content = f"""
|
full_content = f"""
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
@@ -188,7 +188,7 @@ default = true
|
|||||||
|
|
||||||
def test_missing_schema_section(self, tmp_path):
|
def test_missing_schema_section(self, tmp_path):
|
||||||
"""Test error when schema section is missing."""
|
"""Test error when schema section is missing."""
|
||||||
toml_path = tmp_path / "pyproject.toml"
|
toml_path = tmp_path / "settings.toml"
|
||||||
toml_path.write_text("""
|
toml_path.write_text("""
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "test"
|
name = "test"
|
||||||
|
|||||||
Reference in New Issue
Block a user