Skip to content
Open
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
36 changes: 27 additions & 9 deletions codeplain_REST_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Optional

import requests
from requests.exceptions import ConnectionError, Timeout
from requests.exceptions import ConnectionError, RequestException, Timeout

import plain2code_exceptions
from plain2code_state import RunState
Expand Down Expand Up @@ -76,23 +76,41 @@ def _handle_retry_logic(
if not silent:
self.console.error(f"Max retries ({num_retries}) exceeded. Last error: {error}")
if is_connection_error:
raise plain2code_exceptions.NetworkConnectionError("Failed to connect to API server.")
else:
raise error
raise plain2code_exceptions.NetworkConnectionError(
"Connection error: Failed to connect to API server.\n\nPlease check that your internet connection is working."
)
if isinstance(error, RequestException):
raise Exception(f"Error rendering plain code: {error}\n") from error
raise error

def _raise_for_error_code(self, response_json):
"""Raise appropriate exception based on error code in response."""
error_code = response_json.get("error_code")
if error_code not in ERROR_CODE_EXCEPTIONS:
return

exception_class = ERROR_CODE_EXCEPTIONS[error_code]
message = response_json.get("message", "")

# FunctionalRequirementTooComplex has an extra parameter
if error_code == "FunctionalRequirementTooComplex":
raise exception_class(message, response_json.get("proposed_breakdown"))

raise plain2code_exceptions.FunctionalRequirementTooComplex(
message, response_json.get("proposed_breakdown")
)
if error_code == "MissingResource":
raise plain2code_exceptions.MissingResource(f"Missing resource: {message}\n")
if error_code == "ConflictingRequirements":
raise plain2code_exceptions.ConflictingRequirements(f"Conflicting requirements: {message}\n")
if error_code == "CreditBalanceTooLow":
raise plain2code_exceptions.RenderingCreditBalanceTooLow(f"Credit balance too low: {message}\n")
if error_code == "LLMInternalError":
raise plain2code_exceptions.LLMInternalError(f"LLM internal error: {message}\n")
if error_code == "PlainSyntaxError":
raise plain2code_exceptions.plain_syntax_error(message or "Unknown error")
if error_code == "InternalServerError":
raise plain2code_exceptions.InternalServerError(
"Internal server error.\n\n"
f"Please report the error to support@codeplain.ai with the attached {self.log_file_name} file."
)
exception_class = ERROR_CODE_EXCEPTIONS[error_code]
raise exception_class(message)

def post_request(
Expand All @@ -118,7 +136,7 @@ def post_request(
response_json = response.json()
except requests.exceptions.JSONDecodeError as e:
self.console.debug(f"Failed to decode JSON response: {e}. Response text: {response.text}")
raise
raise Exception(f"Error rendering plain code: Failed to decode API response ({e}).\n") from e

if response.status_code == requests.codes.bad_request and "error_code" in response_json:
self._raise_for_error_code(response_json)
Expand Down
2 changes: 1 addition & 1 deletion concept_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def sort_definitions(definitions: list[dict]) -> list[dict]:
msg += "\n".join(cyclic_definitions)
msg += "\n"

raise PlainSyntaxError(msg)
raise PlainSyntaxError(f"Plain syntax error: {msg}")

order = list(nx.topological_sort(concept_graph))
if len(order) > 0:
Expand Down
2 changes: 1 addition & 1 deletion file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def load_linked_resources(template_dirs: list[str], resources_list):
content = open_from(template_dirs, file_name)

if content is None:
raise FileNotFoundError(f"""
raise FileNotFoundError(f"""File not found:
Resource file {file_name} not found. Resource files are searched in the following order (highest to lowest precedence):

1. The directory containing your .plain file
Expand Down
12 changes: 8 additions & 4 deletions module_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,18 @@ def _ensure_module_folders_exist(self, module_name: str, first_render_frid: str)

if not os.path.exists(build_folder_path):
raise MissingPreviousFunctionalitiesError(
"Error rendering plain code: "
f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the source code folder does not exist.\n\n"
f"To fix this, please render the module from the beginning by running:\n"
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}"
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}\n"
)

if not os.path.exists(conformance_tests_path) and self.args.render_conformance_tests:
raise MissingPreviousFunctionalitiesError(
"Error rendering plain code: "
f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the conformance tests folder does not exist.\n\n"
f"To fix this, please render the module from the beginning by running:\n"
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}"
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}\n"
)

return build_folder_path, conformance_tests_path
Expand Down Expand Up @@ -99,18 +101,20 @@ def _ensure_frid_commit_exists(
# Check in build folder
if not git_utils.has_commit_for_frid(build_folder_path, frid, module_name):
raise MissingPreviousFunctionalitiesError(
"Error rendering plain code: "
f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the implementation of the previous functionality ({frid}) hasn't been completed yet.\n\n"
f"To fix this, please render the missing functionality ({frid}) first by running:\n"
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}"
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}\n"
)

# Check in conformance tests folder (only if conformance tests are enabled)
if self.args.render_conformance_tests:
if not git_utils.has_commit_for_frid(conformance_tests_path, frid, module_name):
raise MissingPreviousFunctionalitiesError(
"Error rendering plain code: "
f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the conformance tests for the previous functionality ({frid}) haven't been completed yet.\n\n"
f"To fix this, please render the missing functionality ({frid}) first by running:\n"
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}"
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}\n"
)

def _ensure_previous_frid_commits_exist(
Expand Down
132 changes: 52 additions & 80 deletions plain2code.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

import yaml
from liquid2.exceptions import TemplateNotFoundError
from requests.exceptions import RequestException

import codeplain_REST_api as codeplain_api
import file_utils
Expand All @@ -23,11 +22,8 @@
from plain2code_events import RenderFailed
from plain2code_exceptions import (
ConflictingRequirements,
InternalClientError,
InternalServerError,
InvalidAPIKey,
InvalidFridArgument,
LLMInternalError,
MissingAPIKey,
MissingPreviousFunctionalitiesError,
MissingResource,
Expand Down Expand Up @@ -58,15 +54,20 @@
def print_exit_summary(
run_state: RunState,
spec_filename: str,
error_message: Optional[str] = None,
) -> None:
console.quiet = False
"""Print render outcome after the TUI exits (terminal restored)."""

msg = "\n[#79FC96]✓ rendering completed\n\n" if run_state.render_succeeded else "[#FF6B6B]✗ rendering failed\n\n"
msg += f" [#8E8F91]render id:\t\t\t[#FFFFFF]{run_state.render_id}\n"
msg += f" [#8E8F91]input file:\t\t\t[#FFFFFF]{spec_filename}\n"
msg += f" [#8E8F91]generated code folder:\t[#FFFFFF]{run_state.render_generated_code_path or '-'}\n\n"
msg += f"[#8E8F91]functionalities [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]used credits [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]render time [#FFFFFF]{format_duration_hms(run_state.render_time_accumulated)}\n"
console.info(msg)

if not run_state.render_succeeded and error_message:
console.error(error_message)
console.quiet = True


Expand Down Expand Up @@ -104,20 +105,26 @@ def _get_frids_range(plain_source, start, end=None):
start = str(start)

if start not in frids:
raise InvalidFridArgument(f"Invalid start functionality ID: {start}. Valid IDs are: {frids}.")
raise InvalidFridArgument(
f"Invalid FRID argument: Invalid start functionality ID: {start}. Valid IDs are: {frids}.\n"
)

if end is not None:
end = str(end)
if end not in frids:
raise InvalidFridArgument(f"Invalid end functionality ID: {end}. Valid IDs are: {frids}.")
raise InvalidFridArgument(
f"Invalid FRID argument: Invalid end functionality ID: {end}. Valid IDs are: {frids}.\n"
)

end_idx = frids.index(end) + 1
else:
end_idx = len(frids)

start_idx = frids.index(start)
if start_idx >= end_idx:
raise InvalidFridArgument(f"Start functionality ID: {start} must be before end functionality ID: {end}.")
raise InvalidFridArgument(
f"Invalid FRID argument: Start functionality ID: {start} must be before end functionality ID: {end}.\n"
)

return frids[start_idx:end_idx]

Expand Down Expand Up @@ -186,16 +193,17 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI):

if not response.get("api_key_valid", False):
raise InvalidAPIKey(
"Provided API key is invalid. Please provide a valid API key using the CODEPLAIN_API_KEY environment variable "
"or the --api-key argument."
"Invalid API key: Provided API key is invalid. Please provide a valid API key using the CODEPLAIN_API_KEY environment variable "
"or the --api-key argument.\n"
)

if not response.get("client_version_valid", False):
min_version = response.get("min_client_version", "unknown")
raise OutdatedClientVersion(
"Outdated client version: "
f"Your client version ({system_config.client_version}) is outdated. Minimum required version is {min_version}. "
"Please update using:"
" uv tool upgrade codeplain"
" uv tool upgrade codeplain\n"
)


Expand Down Expand Up @@ -317,84 +325,48 @@ def main(): # noqa: C901
setup_logging(args, event_bus, run_state, args.log_to_file, args.log_file_name, args.filename, args.headless)

exc_info = None
error_message = None

try:
# Validate API key is present
if not args.api_key:
raise MissingAPIKey(
"API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument."
"Missing API key: API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n"
)
render(args, run_state, event_bus)
except InvalidFridArgument as e:
exc_info = sys.exc_info()
console.error(f"Invalid FRID argument: {str(e)}.\n")
except FileNotFoundError as e:
exc_info = sys.exc_info()
console.error(f"File not found: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except TemplateNotFoundError as e:
exc_info = sys.exc_info()
console.error(f"Template not found: {str(e)}\n")
console.error(system_config.get_error_message("template_not_found"))
except PlainSyntaxError as e:
exc_info = sys.exc_info()
console.error(f"Plain syntax error: {str(e)}\n")
except KeyboardInterrupt:
exc_info = sys.exc_info()
console.error("Keyboard interrupt")
console.debug(f"Render ID: {run_state.render_id}")
except RequestException as e:
exc_info = sys.exc_info()
console.error(f"Error rendering plain code: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except MissingPreviousFunctionalitiesError as e:
exc_info = sys.exc_info()
console.error(f"Error rendering plain code: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except MissingAPIKey as e:
console.error(f"Missing API key: {str(e)}\n")
except InvalidAPIKey as e:
console.error(f"Invalid API key: {str(e)}\n")
except OutdatedClientVersion as e:
console.error(f"Outdated client version: {str(e)}\n")
except (InternalServerError, InternalClientError):
exc_info = sys.exc_info()
console.error(
f"Internal server error.\n\nPlease report the error to support@codeplain.ai with the attached {args.log_file_name} file."
)
console.debug(f"Render ID: {run_state.render_id}")
except ConflictingRequirements as e:
exc_info = sys.exc_info()
console.error(f"Conflicting requirements: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except RenderingCreditBalanceTooLow as e:
exc_info = sys.exc_info()
console.error(f"Credit balance too low: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except LLMInternalError as e:
exc_info = sys.exc_info()
console.error(f"LLM internal error: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except MissingResource as e:
exc_info = sys.exc_info()
console.error(f"Missing resource: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except NetworkConnectionError as e:
exc_info = sys.exc_info()
console.error(f"Connection error: {str(e)}\n")
console.error("Please check that your internet connection is working.")
except ModuleDoesNotExistError as e:
exc_info = sys.exc_info()
console.error(f"Module does not exist: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except Exception as e:
exc_info = sys.exc_info()
console.error(f"Error rendering plain code: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except BaseException as e:
if isinstance(e, KeyboardInterrupt):
error_message = "Keyboard interrupt"
else:
error_message = str(e) if str(e) else repr(e)

if not isinstance(
e,
(
InvalidFridArgument,
FileNotFoundError,
MissingResource,
TemplateNotFoundError,
PlainSyntaxError,
MissingPreviousFunctionalitiesError,
MissingAPIKey,
InvalidAPIKey,
OutdatedClientVersion,
ConflictingRequirements,
RenderingCreditBalanceTooLow,
NetworkConnectionError,
ModuleDoesNotExistError,
),
):
exc_info = sys.exc_info()
finally:
print_exit_summary(run_state, args.filename)
print_exit_summary(
run_state,
args.filename,
error_message=error_message,
)
if exc_info:
# Log traceback using the logging system
logging.error("Render crashed with exception:", exc_info=exc_info)
# Log traceback
dump_crash_logs(args)


Expand Down
4 changes: 2 additions & 2 deletions plain2code_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def process_test_script_path(script_arg_name, config):
if isinstance(script_input_path, str) and script_input_path.startswith("/"):
if not os.path.exists(script_input_path):
raise FileNotFoundError(
f"Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file."
f"File not found: Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file.\n"
)
return config

Expand All @@ -47,7 +47,7 @@ def process_test_script_path(script_arg_name, config):
setattr(config, script_arg_name, renderer_relative_path)
else:
raise FileNotFoundError(
f"Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file."
f"File not found: Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file.\n"
)
return config

Expand Down
17 changes: 14 additions & 3 deletions plain2code_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,20 @@ def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
try:
template = context.env.get_template(str(name), context=context, tag=self.tag, whitespaces=whitespaces)
except TemplateNotFoundError as err:
err.token = self.name.token
err.template_name = context.template.full_name()
raise
detail = str(err)
long_message = f"""Template not found: {detail}
The required template could not be found. Templates are searched in the following order (highest to lowest precedence):

1. The directory containing your .plain file
2. The directory specified by --template-dir (if provided)
3. The built-in 'standard_template_library' directory

Please ensure that the missing template exists in one of these locations, or specify the correct --template-dir if using custom templates.
"""
wrapped = TemplateNotFoundError(long_message)
wrapped.token = self.name.token
wrapped.template_name = context.template.full_name()
raise wrapped from err

namespace: dict[str, object] = dict(arg.evaluate(context) for arg in self.args)

Expand Down
Loading
Loading