Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
7 changes: 7 additions & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
These are Lead developer instructions for AI Agents :
- avoid except:pass handles, better crash with a clear error message than hide the problems.
- Use testing-driven development.
- Fix library code, not tests
- Never perform git commits
- Keep line length under 110 characters
- Finish your changes by running `make qa` in the boulder conda env.
71 changes: 51 additions & 20 deletions boulder/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import os

import dash
Expand All @@ -10,7 +11,6 @@
)
from .config import (
get_config_from_path_with_comments,
get_initial_config,
get_initial_config_with_comments,
)
from .layout import get_layout
Expand Down Expand Up @@ -43,24 +43,20 @@
server = app.server # Expose the server for deployment

# Load initial configuration with optional override via environment variable
try:
# Allow overriding the initial configuration via environment variable
# Use either BOULDER_CONFIG_PATH or BOULDER_CONFIG for convenience
env_config_path = os.environ.get("BOULDER_CONFIG_PATH") or os.environ.get(
"BOULDER_CONFIG"
)

if env_config_path and env_config_path.strip():
cleaned = env_config_path.strip()
initial_config, original_yaml = get_config_from_path_with_comments(cleaned)
# When a specific file is provided, propagate its base name to the UI store
provided_filename = os.path.basename(cleaned)
else:
initial_config, original_yaml = get_initial_config_with_comments()
except Exception as e:
print(f"Warning: Could not load config with comments, using standard loader: {e}")
initial_config = get_initial_config()
original_yaml = ""
# Allow overriding the initial configuration via environment variable
# Use either BOULDER_CONFIG_PATH or BOULDER_CONFIG for convenience
env_config_path = os.environ.get("BOULDER_CONFIG_PATH") or os.environ.get(
"BOULDER_CONFIG"
)

if env_config_path and env_config_path.strip():
cleaned = env_config_path.strip()
initial_config, original_yaml = get_config_from_path_with_comments(cleaned)
# When a specific file is provided, propagate its base name to the UI store
provided_filename = os.path.basename(cleaned)
else:
initial_config, original_yaml = get_initial_config_with_comments()


# Set the layout
app.layout = get_layout(
Expand All @@ -74,6 +70,41 @@
callbacks.register_callbacks(app)


def run_server(debug: bool = False, host: str = "0.0.0.0", port: int = 8050) -> None:
def run_server(
debug: bool = False, host: str = "0.0.0.0", port: int = 8050, verbose: bool = False
) -> None:
"""Run the Dash server."""
if verbose:
# Configure logging for verbose output
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
logger.info("Boulder server starting in verbose mode")
logger.info(f"Server configuration: host={host}, port={port}, debug={debug}")

# Check for potential port conflicts and log them
import socket

try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind((host, port))
logger.info(f"Port {port} is available for binding")
except OSError as e:
logger.warning(
f"Port {port} binding check failed: {e} "
f"(this is normal if CLI already handled port conflicts)"
)

# Log initial configuration details
env_config_path = os.environ.get("BOULDER_CONFIG_PATH") or os.environ.get(
"BOULDER_CONFIG"
)
if env_config_path:
logger.info(f"Loading configuration from: {env_config_path}")
else:
logger.info("Using default configuration")

app.run(debug=debug, host=host, port=port)
25 changes: 20 additions & 5 deletions boulder/callbacks/config_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import yaml
from dash import Input, Output, State, dcc, html

from ..verbose_utils import get_verbose_logger, is_verbose_mode

logger = get_verbose_logger(__name__)

# Configure YAML to preserve dict order without Python tags
yaml.add_representer(
dict,
Expand Down Expand Up @@ -87,6 +91,7 @@ def render_config_upload_area(file_name: str) -> tuple:
Output("current-config", "data"),
Output("config-file-name", "data"),
Output("original-yaml-with-comments", "data"),
Output("upload-config", "contents"), # Add this to reset upload contents
],
[
Input("upload-config", "contents"),
Expand Down Expand Up @@ -119,6 +124,7 @@ def handle_config_upload_delete(
from ..config import (
load_yaml_string_with_comments,
normalize_config,
validate_config,
)

# Use comment-preserving YAML loader
Expand All @@ -130,18 +136,27 @@ def handle_config_upload_delete(

# Normalize from YAML with 🪨 STONE standard to internal format
normalized = normalize_config(decoded)
return normalized, upload_filename, decoded_string
# Validate the configuration (this will also convert units)
normalized = validate_config(normalized)
if is_verbose_mode():
logger.info(
f"Successfully loaded configuration file: {upload_filename}"
)
return normalized, upload_filename, decoded_string, dash.no_update
else:
print(
"Only YAML format with 🪨 STONE standard (.yaml/.yml) files are supported. Got:"
f" {upload_filename}"
)
return dash.no_update, "", ""
return dash.no_update, "", "", dash.no_update
except Exception as e:
print(f"Error processing uploaded file: {e}")
return dash.no_update, "", ""
if is_verbose_mode():
logger.error(f"Error processing uploaded file: {e}", exc_info=True)
else:
print(f"Error processing uploaded file: {e}")
return dash.no_update, "", "", dash.no_update
elif trigger == "delete-config-file" and delete_n_clicks:
return get_initial_config(), "", ""
return get_initial_config(), "", "", None # Reset upload contents to None
else:
raise dash.exceptions.PreventUpdate

Expand Down
47 changes: 22 additions & 25 deletions boulder/callbacks/modal_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,31 +118,23 @@ def open_config_yaml_modal(

# If we have original YAML with comments, try to preserve them
if original_yaml and original_yaml.strip():
try:
# Load original YAML with comments
original_data = load_yaml_string_with_comments(original_yaml)

# Check if the config has actually changed by comparing the original with new stone config
original_normalized = normalize_config(original_data)
if original_normalized == config:
# Config hasn't changed, use original YAML directly
yaml_str = original_yaml
else:
# Config has changed, update while preserving comments
updated_data = _update_yaml_preserving_comments(
original_data, stone_config
)
yaml_str = yaml_to_string_with_comments(updated_data)
except Exception as e:
print(f"Warning: Could not preserve comments: {e}")
# Fallback to standard format
yaml_str = yaml_to_string_with_comments(stone_config)
# Load original YAML with comments
original_data = load_yaml_string_with_comments(original_yaml)

# Check if the config has actually changed by comparing the original with new stone config
original_normalized = normalize_config(original_data)
if original_normalized == config:
# Config hasn't changed, use original YAML directly
yaml_str = original_yaml
else:
# Config has changed, update while preserving comments
updated_data = _update_yaml_preserving_comments(
original_data, stone_config
)
yaml_str = yaml_to_string_with_comments(updated_data)
else:
# No original YAML, use standard format
try:
yaml_str = yaml_to_string_with_comments(stone_config)
except Exception:
yaml_str = yaml.dump(stone_config, sort_keys=False, indent=2)
yaml_str = yaml_to_string_with_comments(stone_config)

textarea = dcc.Textarea(
id="config-yaml-editor",
Expand Down Expand Up @@ -180,7 +172,11 @@ def update_config_from_yaml(n_clicks: int, yaml_str: str) -> Tuple[dict, bool, s
raise dash.exceptions.PreventUpdate

try:
from ..config import load_yaml_string_with_comments, normalize_config
from ..config import (
load_yaml_string_with_comments,
normalize_config,
validate_config,
)

# Try to use comment-preserving YAML loader first
try:
Expand All @@ -190,8 +186,9 @@ def update_config_from_yaml(n_clicks: int, yaml_str: str) -> Tuple[dict, bool, s
new_config = yaml.safe_load(yaml_str)

normalized_config = normalize_config(new_config)
validated_config = validate_config(normalized_config)
# Update the original YAML store with the new YAML string to preserve comments for future edits
return normalized_config, False, yaml_str
return validated_config, False, yaml_str
except yaml.YAMLError as e:
print(f"YAML Error on save: {e}")
# In a real app, you'd show an error to the user here
Expand Down
31 changes: 29 additions & 2 deletions boulder/callbacks/notification_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import dash
from dash import Input, Output, State

from ..verbose_utils import get_verbose_logger, is_verbose_mode

logger = get_verbose_logger(__name__)


def register_callbacks(app) -> None: # type: ignore
"""Register notification-related callbacks."""
Expand Down Expand Up @@ -44,13 +48,30 @@ def notification_handler(

# Config upload
if trigger == "upload-config" and upload_contents:
if is_verbose_mode():
logger.info(f"Processing uploaded config file: {upload_filename}")
try:
import yaml

content_type, content_string = upload_contents.split(",")
decoded_string = base64.b64decode(content_string).decode("utf-8")

if is_verbose_mode():
logger.info(
f"File content preview (first 200 chars): {decoded_string[:200]}..."
)

# Validate as YAML (STONE standard) instead of JSON
yaml.safe_load(decoded_string)
parsed_yaml = yaml.safe_load(decoded_string)

if is_verbose_mode():
keys_info = (
list(parsed_yaml.keys())
if isinstance(parsed_yaml, dict)
else "Not a dict"
)
logger.info(f"YAML parsed successfully. Keys: {keys_info}")

return (
True,
f"✅ Configuration loaded from {upload_filename}",
Expand All @@ -59,7 +80,13 @@ def notification_handler(
)
except Exception as e:
message = f"Could not parse file {upload_filename}. Error: {e}"
print(f"ERROR: {message}")
if is_verbose_mode():
logger.error(
f"File upload failed for {upload_filename}: {message}",
exc_info=True,
)
else:
print(f"ERROR: {message}")
return (
True,
message,
Expand Down
Loading
Loading