Skip to content

Commit 665f8a5

Browse files
Add connection check before starting rendering
1 parent 7b5a5f6 commit 665f8a5

6 files changed

Lines changed: 86 additions & 33 deletions

File tree

codeplain_REST_api.py

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@
1414
"LLMInternalError",
1515
]
1616

17+
# Mapping from API error codes to exception classes
18+
ERROR_CODE_EXCEPTIONS = {
19+
"FunctionalRequirementTooComplex": plain2code_exceptions.FunctionalRequirementTooComplex,
20+
"ConflictingRequirements": plain2code_exceptions.ConflictingRequirements,
21+
"CreditBalanceTooLow": plain2code_exceptions.CreditBalanceTooLow,
22+
"LLMInternalError": plain2code_exceptions.LLMInternalError,
23+
"MissingResource": plain2code_exceptions.MissingResource,
24+
"PlainSyntaxError": plain2code_exceptions.PlainSyntaxError,
25+
"InternalServerError": plain2code_exceptions.InternalServerError,
26+
}
27+
1728

1829
class CodeplainAPI:
1930

@@ -33,13 +44,31 @@ def _extend_payload_with_run_state(self, payload: dict, run_state: RunState):
3344
run_state.increment_call_count()
3445
payload["render_state"] = run_state.to_dict()
3546

36-
def post_request(self, endpoint_url, headers, payload, run_state: Optional[RunState]): # noqa: C901
47+
def _raise_for_error_code(self, response_json):
48+
"""Raise appropriate exception based on error code in response."""
49+
error_code = response_json.get("error_code")
50+
if error_code not in ERROR_CODE_EXCEPTIONS:
51+
return
52+
53+
exception_class = ERROR_CODE_EXCEPTIONS[error_code]
54+
message = response_json.get("message", "")
55+
56+
# FunctionalRequirementTooComplex has an extra parameter
57+
if error_code == "FunctionalRequirementTooComplex":
58+
raise exception_class(message, response_json.get("proposed_breakdown"))
59+
60+
raise exception_class(message)
61+
62+
def post_request(
63+
self, endpoint_url, headers, payload, run_state: Optional[RunState], num_retries: int = MAX_RETRIES
64+
):
3765
if run_state is not None:
3866
self._extend_payload_with_run_state(payload, run_state)
3967

4068
retry_delay = RETRY_DELAY
4169
response_json = None
42-
for attempt in range(MAX_RETRIES + 1):
70+
71+
for attempt in range(num_retries + 1):
4372
try:
4473
response = requests.post(endpoint_url, headers=headers, json=payload)
4574

@@ -50,28 +79,7 @@ def post_request(self, endpoint_url, headers, payload, run_state: Optional[RunSt
5079
raise
5180

5281
if response.status_code == requests.codes.bad_request and "error_code" in response_json:
53-
if response_json["error_code"] == "FunctionalRequirementTooComplex":
54-
raise plain2code_exceptions.FunctionalRequirementTooComplex(
55-
response_json["message"], response_json.get("proposed_breakdown")
56-
)
57-
58-
if response_json["error_code"] == "ConflictingRequirements":
59-
raise plain2code_exceptions.ConflictingRequirements(response_json["message"])
60-
61-
if response_json["error_code"] == "CreditBalanceTooLow":
62-
raise plain2code_exceptions.CreditBalanceTooLow(response_json["message"])
63-
64-
if response_json["error_code"] == "LLMInternalError":
65-
raise plain2code_exceptions.LLMInternalError(response_json["message"])
66-
67-
if response_json["error_code"] == "MissingResource":
68-
raise plain2code_exceptions.MissingResource(response_json["message"])
69-
70-
if response_json["error_code"] == "PlainSyntaxError":
71-
raise plain2code_exceptions.PlainSyntaxError(response_json["message"])
72-
73-
if response_json["error_code"] == "InternalServerError":
74-
raise plain2code_exceptions.InternalServerError(response_json["message"])
82+
self._raise_for_error_code(response_json)
7583

7684
response.raise_for_status()
7785
return response_json
@@ -81,16 +89,24 @@ def post_request(self, endpoint_url, headers, payload, run_state: Optional[RunSt
8189
if response_json["error_code"] not in RETRY_ERROR_CODES:
8290
raise e
8391

84-
if attempt < MAX_RETRIES:
85-
self.console.info(f"Error on attempt {attempt + 1}/{MAX_RETRIES + 1}: {e}")
92+
if attempt < num_retries:
93+
self.console.info(f"Error on attempt {attempt + 1}/{num_retries + 1}: {e}")
8694
self.console.info(f"Retrying in {retry_delay} seconds...")
8795
time.sleep(retry_delay)
88-
# Exponential backoff
89-
retry_delay *= 2
96+
retry_delay *= 2 # Exponential backoff
9097
else:
91-
self.console.error(f"Max retries ({MAX_RETRIES}) exceeded. Last error: {e}")
98+
self.console.error(f"Max retries ({num_retries}) exceeded. Last error: {e}")
9299
raise e
93100

101+
def connection_check(self, client_version):
102+
endpoint_url = f"{self.api_url}/connection_check"
103+
headers = {"Content-Type": "application/json"}
104+
payload = {
105+
"api_key": self.api_key,
106+
"client_version": client_version,
107+
}
108+
return self.post_request(endpoint_url, headers, payload, None, num_retries=0)
109+
94110
def render_functional_requirement(
95111
self,
96112
frid: str,

config/system_config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
client_version: "0.17.0"
2+
13
system_requirements:
24
timeout:
35
command: timeout

plain2code.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
ConflictingRequirements,
2222
CreditBalanceTooLow,
2323
InternalServerError,
24+
InvalidAPIKey,
2425
InvalidFridArgument,
2526
LLMInternalError,
2627
MissingAPIKey,
2728
MissingPreviousFunctionalitiesError,
2829
MissingResource,
30+
OutdatedClientVersion,
2931
PlainSyntaxError,
3032
UnexpectedState,
3133
)
@@ -163,6 +165,25 @@ def setup_logging(
163165
root_logger.info(f"Render ID: {render_id}") # Ensure render ID is logged in to codeplain.log file
164166

165167

168+
def _check_connection(codeplainAPI):
169+
"""Check API connectivity and validate API key and client version."""
170+
response = codeplainAPI.connection_check(system_config.client_version)
171+
172+
if not response.get("api_key_valid", False):
173+
raise InvalidAPIKey(
174+
"Provided API key is invalid. Please provide a valid API key using the CODEPLAIN_API_KEY environment variable "
175+
"or the --api-key argument."
176+
)
177+
178+
if not response.get("client_version_valid", False):
179+
min_version = response.get("min_client_version", "unknown")
180+
raise OutdatedClientVersion(
181+
f"Your client version ({system_config.client_version}) is outdated. Minimum required version is {min_version}. "
182+
"Please update using:"
183+
" uv tool upgrade codeplain"
184+
)
185+
186+
166187
def render(args, run_state: RunState, codeplain_api, event_bus: EventBus): # noqa: C901
167188
# Check system requirements before proceeding
168189
system_config.verify_requirements()
@@ -187,6 +208,8 @@ def render(args, run_state: RunState, codeplain_api, event_bus: EventBus): # no
187208
assert args.api is not None and args.api != "", "API URL is required"
188209
codeplainAPI.api_url = args.api
189210

211+
_check_connection(codeplainAPI)
212+
190213
module_renderer = ModuleRenderer(
191214
codeplainAPI,
192215
args.filename,
@@ -266,6 +289,10 @@ def main(): # noqa: C901
266289
console.debug(f"Render ID: {run_state.render_id}")
267290
except MissingAPIKey as e:
268291
console.error(f"Missing API key: {str(e)}\n")
292+
except InvalidAPIKey as e:
293+
console.error(f"Invalid API key: {str(e)}\n")
294+
except OutdatedClientVersion as e:
295+
console.error(f"Outdated client version: {str(e)}\n")
269296
except (InternalServerError, UnexpectedState):
270297
exc_info = sys.exc_info()
271298
console.error(

plain2code_exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ class MissingAPIKey(Exception):
3333
pass
3434

3535

36+
class InvalidAPIKey(Exception):
37+
pass
38+
39+
40+
class OutdatedClientVersion(Exception):
41+
pass
42+
43+
3644
class InvalidFridArgument(Exception):
3745
pass
3846

system_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ class SystemConfig:
1212

1313
def __init__(self):
1414
self.config = self._load_config()
15+
if "client_version" not in self.config:
16+
raise KeyError("Missing 'client_version' in system_config.yaml")
1517
if "system_requirements" not in self.config:
1618
raise KeyError("Missing 'system_requirements' section in system_config.yaml")
1719
if "error_messages" not in self.config:
1820
raise KeyError("Missing 'error_messages' section in system_config.yaml")
1921

22+
self.client_version = self.config["client_version"]
2023
self.requirements = self.config["system_requirements"]
2124
self.error_messages = self.config["error_messages"]
2225

tui/components.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,9 @@ def get_max_label_width(active_types: list["ScriptOutputType"]) -> int:
4141
return 0
4242
return max(len(script_type.value) for script_type in active_types)
4343

44-
def get_padded_label(self, active_types: list["ScriptOutputType"]) -> str:
44+
def get_padded_label(self) -> str:
4545
"""Get the label left-aligned (no padding).
4646
47-
Args:
48-
active_types: List of ScriptOutputType enum members that are currently active
49-
5047
Returns:
5148
Label without padding (left-aligned)
5249
"""

0 commit comments

Comments
 (0)