broadfield-dev commited on
Commit
6dcf3d6
·
verified ·
1 Parent(s): 5a39165

Update build_logic.py

Browse files
Files changed (1) hide show
  1. build_logic.py +337 -111
build_logic.py CHANGED
@@ -67,54 +67,99 @@ def parse_markdown(markdown_input):
67
  in_code_block = False
68
 
69
  lines = markdown_input.strip().split("\n")
70
- if any(("# # Space:" in line.strip() or "# Space:" in line.strip()) for line in lines):
71
- cleaned_lines_temp = []
72
- for line_idx, line in enumerate(lines):
73
- if line.startswith("# ") and (
74
- "# Space:" in line or "### File:" in line or "## File Structure" in line or
75
- (line_idx > 0 and "### File:" in lines[line_idx-1])
76
- ):
77
- cleaned_lines_temp.append(line[2:])
78
- else:
79
- cleaned_lines_temp.append(line)
80
- lines = cleaned_lines_temp
 
 
 
 
 
 
81
 
82
  for line_content_orig in lines:
83
  line_content_stripped = line_content_orig.strip()
84
 
85
  if line_content_stripped.startswith("### File:"):
86
- if current_file_path and in_file_definition:
 
87
  space_info["files"].append({"path": current_file_path, "content": "\n".join(current_file_content_lines).strip()})
 
88
  current_file_path = line_content_stripped.replace("### File:", "").strip()
 
89
  current_file_path = re.split(r'\s*\(', current_file_path, 1)[0].strip()
 
 
 
 
90
  current_file_content_lines = []
91
  in_file_definition = True
92
- in_code_block = False
93
  continue
94
 
 
95
  if not in_file_definition:
96
  if line_content_stripped.startswith("# Space:"):
97
  full_space_name_md = line_content_stripped.replace("# Space:", "").strip()
98
  if "/" in full_space_name_md:
99
- space_info["owner_md"], space_info["repo_name_md"] = full_space_name_md.split("/", 1)
 
 
 
 
100
  else:
101
  space_info["repo_name_md"] = full_space_name_md
 
102
  continue
103
 
 
104
  if in_file_definition:
105
  if line_content_stripped.startswith("```"):
 
106
  in_code_block = not in_code_block
107
- continue
 
 
 
 
 
 
 
108
 
 
109
  if in_code_block:
110
  current_file_content_lines.append(line_content_orig)
111
- elif not in_code_block and line_content_stripped.startswith("[Binary file"):
 
 
112
  current_file_content_lines.append(line_content_orig)
 
 
113
 
114
- if current_file_path and in_file_definition:
 
 
115
  space_info["files"].append({"path": current_file_path, "content": "\n".join(current_file_content_lines).strip()})
116
 
117
- space_info["files"] = [f for f in space_info["files"] if f.get("path")]
 
 
 
 
 
 
 
 
 
 
118
  return space_info
119
 
120
 
@@ -124,6 +169,7 @@ def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui
124
  sdk = None
125
  files = []
126
  error = None
 
127
 
128
  try:
129
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
@@ -131,51 +177,66 @@ def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui
131
 
132
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
133
  if err_repo_id: return None, None, err_repo_id
134
- repo_id_for_error_logging = repo_id
135
 
136
  api = HfApi(token=resolved_api_token)
137
- repo_info_obj = api.repo_info(repo_id=repo_id, repo_type="space")
 
138
  sdk = repo_info_obj.sdk
139
  files = [sibling.rfilename for sibling in repo_info_obj.siblings if sibling.rfilename]
 
140
  if not files and repo_info_obj.siblings:
141
  logger.warning(f"Repo {repo_id} has siblings but no rfilenames extracted.")
 
142
  except HfHubHTTPError as e_http: # Catch specific HF HTTP errors first
143
- logger.error(f"HTTP error getting repo info for {repo_id_for_error_logging}: {e_http}")
144
  error_message = str(e_http)
145
- if e_http.response is not None: # Check if response object exists
146
- if e_http.response.status_code == 404:
147
- error = f"Space '{repo_id_for_error_logging}' not found (404)."
148
- elif e_http.response.status_code in (401,403):
149
- error = f"Access denied for '{repo_id_for_error_logging}' (401/403). Check token permissions."
150
- else:
151
- error = f"HTTP Error {e_http.response.status_code} for '{repo_id_for_error_logging}': {error_message}"
152
- else: # No response object, use generic message
153
- error = f"Network or HTTP Error for '{repo_id_for_error_logging}': {error_message}"
154
  except Exception as e: # Catch other general exceptions
155
- logger.warning(f"Could not get full repo_info for {repo_id_for_error_logging}, falling back to list_repo_files: {e}")
 
 
 
156
  try:
 
157
  resolved_api_token_fb, token_err_fb = _get_api_token(ui_api_token_from_textbox)
158
- if token_err_fb: return None, None, token_err_fb # Propagate error
159
  repo_id_fb, err_repo_id_fb = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
160
- if err_repo_id_fb: return None, None, err_repo_id_fb # Propagate error
161
- files = list_repo_files(repo_id=repo_id_fb, token=resolved_api_token_fb, repo_type="space")
 
 
 
 
 
162
  except HfHubHTTPError as e2_http:
163
- logger.error(f"HTTP error during fallback list_repo_files for {repo_id_for_error_logging}: {e2_http}")
164
  error_message_fb = str(e2_http)
165
- if e2_http.response is not None:
166
- if e2_http.response.status_code == 404:
167
- error = f"Space '{repo_id_for_error_logging}' not found during fallback (404)."
168
- else:
169
- error = f"HTTP Error {e2_http.response.status_code} for '{repo_id_for_error_logging}' during fallback: {error_message_fb}"
170
  else:
171
- error = f"Network or HTTP Error for '{repo_id_for_error_logging}' during fallback: {error_message_fb}"
 
 
172
  except Exception as e2:
173
- logger.exception(f"Error listing files for {repo_id_for_error_logging} during fallback: {e2}")
174
- error = f"Error listing files for `{repo_id_for_error_logging}`: {str(e2)}"
 
175
 
176
 
 
177
  if not files and not error:
178
- error = f"No files found in Space `{repo_id_for_error_logging}` (or an issue fetching them)."
 
179
  return sdk, files, error
180
 
181
 
@@ -188,6 +249,7 @@ def list_space_files_for_browsing(ui_api_token_from_textbox, space_name_ui, owne
188
  # --- Function to Fetch File Content from Hub ---
189
  def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo):
190
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
 
191
  try:
192
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
193
  if token_err: return None, token_err
@@ -195,170 +257,334 @@ def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, f
195
  if err_repo_id: return None, err_repo_id
196
  repo_id_for_error_logging = repo_id
197
  if not file_path_in_repo: return None, "Error: File path cannot be empty."
198
- downloaded_file_path = hf_hub_download(repo_id=repo_id, filename=file_path_in_repo, repo_type="space", token=resolved_api_token)
 
 
 
 
 
 
 
 
 
 
 
199
  content = Path(downloaded_file_path).read_text(encoding="utf-8")
200
  return content, None
 
 
 
 
 
201
  except HfHubHTTPError as e_http:
202
- logger.error(f"HTTP error fetching file {file_path_in_repo} from {repo_id_for_error_logging}: {e_http}")
203
  error_message = str(e_http)
204
- if e_http.response is not None and e_http.response.status_code == 404:
205
- return None, f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging}' (404)."
 
 
 
206
  return None, f"HTTP Error fetching file '{file_path_in_repo}': {error_message}"
207
  except Exception as e:
208
- logger.exception(f"Error fetching file content for {file_path_in_repo} from {repo_id_for_error_logging}:")
209
  return None, f"Error fetching file content: {str(e)}"
210
 
211
  # --- Create/Update Space ---
212
  def create_space(ui_api_token_from_textbox, space_name_ui, owner_ui, sdk_ui, markdown_input):
213
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
 
214
  try:
215
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
216
  if token_err: return token_err
217
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
218
  if err_repo_id: return err_repo_id
219
- repo_id_for_error_logging = repo_id
 
220
  space_info = parse_markdown(markdown_input)
221
 
222
  with tempfile.TemporaryDirectory() as temp_dir:
223
  repo_staging_path = Path(temp_dir) / "repo_staging_content"
224
  repo_staging_path.mkdir(exist_ok=True)
225
- if not space_info["files"]:
226
- with open(repo_staging_path / ".gitattributes", "w") as f: f.write("* text=auto eol=lf\n")
227
- logger.info(f"Markdown contained no files. Staging dummy .gitattributes for {repo_id}.")
 
 
 
 
 
 
 
 
 
228
  for file_info in space_info["files"]:
229
- if not file_info.get("path"):
230
- logger.warning(f"Skipping file_info with no path: {file_info}")
 
231
  continue
232
- file_path_abs = repo_staging_path / file_info["path"]
233
- file_path_abs.parent.mkdir(parents=True, exist_ok=True)
234
  content_to_write = file_info.get("content", "")
235
- with open(file_path_abs, "w", encoding="utf-8") as f: f.write(content_to_write)
236
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  create_repo(repo_id=repo_id, token=resolved_api_token, repo_type="space", space_sdk=sdk_ui, private=False, exist_ok=True)
238
-
 
 
 
239
  try:
240
- api = HfApi(token=resolved_api_token)
241
  current_hub_files_info = api.list_repo_files(repo_id=repo_id, repo_type="space", recursive=True)
242
  current_hub_files = set(current_hub_files_info)
243
- markdown_filenames = set(f_info["path"] for f_info in space_info["files"])
244
- files_to_delete_on_hub = list(current_hub_files - markdown_filenames)
245
- files_to_delete_on_hub = [f for f in files_to_delete_on_hub if not (f.startswith('.git') or (f == "README.md" and "README.md" not in markdown_filenames))]
 
 
 
 
 
246
 
247
 
248
  if files_to_delete_on_hub:
249
- logger.info(f"Deleting {len(files_to_delete_on_hub)} files from {repo_id} not in new markdown: {files_to_delete_on_hub}")
250
  delete_operations = [CommitOperationDelete(path_in_repo=f) for f in files_to_delete_on_hub]
251
- if delete_operations:
252
- api.create_commit(
253
- repo_id=repo_id,
254
- repo_type="space",
255
- operations=delete_operations,
256
- commit_message=f"AI Space Builder: Removed {len(files_to_delete_on_hub)} files not in updated structure."
257
- )
258
- except Exception as e_delete_old:
259
- logger.error(f"Error during pre-upload deletion of old files in {repo_id}: {e_delete_old}. Proceeding with upload.")
260
-
261
- upload_folder(repo_id=repo_id, folder_path=str(repo_staging_path), path_in_repo=".", token=resolved_api_token, repo_type="space", commit_message=f"Space content update for {repo_id} via AI Builder")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  return f"Successfully created/updated Space: [{repo_id}](https://huggingface.co/spaces/{repo_id})"
 
 
 
 
 
 
 
 
 
 
263
  except Exception as e:
264
- logger.exception(f"Error in create_space for {repo_id_for_error_logging}:")
265
  return f"Error during Space creation/update: {str(e)}"
266
 
267
  # --- Update Single File ---
268
  def update_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, file_content, commit_message_ui):
269
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
 
270
  try:
271
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
272
  if token_err: return token_err
273
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
274
  if err_repo_id: return err_repo_id
275
- repo_id_for_error_logging = repo_id
 
276
  if not file_path_in_repo: return "Error: File Path to update cannot be empty."
277
- file_path_in_repo = file_path_in_repo.lstrip('/').replace(os.sep, '/')
278
  commit_msg = commit_message_ui or f"Update {file_path_in_repo} via AI Space Editor"
 
279
  api = HfApi(token=resolved_api_token)
280
- file_content_bytes = file_content.encode('utf-8')
281
- with tempfile.NamedTemporaryFile(delete=False) as tmp_file_obj:
282
- tmp_file_obj.write(file_content_bytes)
 
283
  tmp_file_path = tmp_file_obj.name
 
284
  try:
285
- api.upload_file(path_or_fileobj=tmp_file_path, path_in_repo=file_path_in_repo, repo_id=repo_id, repo_type="space", commit_message=commit_msg)
 
 
 
 
 
 
 
286
  return f"Successfully updated `{file_path_in_repo}` in Space [{repo_id}](https://huggingface.co/spaces/{repo_id})"
287
  finally:
288
- os.remove(tmp_file_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  except Exception as e:
290
- logger.exception(f"Error in update_space_file for {repo_id_for_error_logging}, file {file_path_in_repo}:")
291
- return f"Error updating file for `{repo_id_for_error_logging}`: {str(e)}"
 
292
 
293
  # --- Delete Single File ---
294
  def delete_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, commit_message_ui=None):
295
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
 
296
  try:
297
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
298
  if token_err: return f"API Token Error: {token_err}"
299
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
300
  if err_repo_id: return f"Repo ID Error: {err_repo_id}"
301
- repo_id_for_error_logging = repo_id
 
302
  if not file_path_in_repo: return "Error: File path cannot be empty for deletion."
 
 
 
 
 
303
  effective_commit_message = commit_message_ui or f"Deleted file: {file_path_in_repo} via AI Space Editor"
304
- hf_delete_file(path_in_repo=file_path_in_repo, repo_id=repo_id, repo_type="space", token=resolved_api_token, commit_message=effective_commit_message)
 
 
 
 
 
 
 
 
305
  return f"Successfully deleted file: {file_path_in_repo}"
 
306
  except HfHubHTTPError as e_http: # Catch specific HF HTTP errors
307
- logger.error(f"HTTP error deleting file {file_path_in_repo} from {repo_id_for_error_logging}: {e_http}")
308
  error_message = str(e_http)
309
- if e_http.response is not None and e_http.response.status_code == 404:
310
- return f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging}' for deletion (404)."
311
- return f"HTTP Error deleting file '{file_path_in_repo}': {error_message}"
 
 
 
 
312
  except Exception as e:
313
- logger.exception(f"Error deleting file {file_path_in_repo} from {repo_id_for_error_logging}:")
314
  return f"Error deleting file '{file_path_in_repo}': {str(e)}"
315
 
316
  # --- Get Space Runtime Status ---
317
  def get_space_runtime_status(ui_api_token_from_textbox, space_name_ui, owner_ui):
318
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
 
319
  try:
320
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
321
  if token_err: return None, f"API Token Error: {token_err}"
322
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
323
  if err_repo_id: return None, f"Repo ID Error: {err_repo_id}"
324
- repo_id_for_error_logging = repo_id
 
325
  api = HfApi(token=resolved_api_token)
326
  logger.info(f"Fetching runtime status for Space: {repo_id}")
327
-
 
328
  runtime_info = api.get_space_runtime(repo_id=repo_id)
329
-
 
330
  status_details = {
331
  "stage": runtime_info.stage,
332
  "hardware": runtime_info.hardware,
333
- "requested_hardware": runtime_info.requested_hardware,
334
- "error_message": None,
335
  "full_log_link": f"https://huggingface.co/spaces/{repo_id}/logs",
336
- "raw_data": runtime_info.raw
337
  }
 
 
338
  if runtime_info.stage == "ERRORED":
339
  error_content = None
 
340
  if hasattr(runtime_info, 'error') and runtime_info.error: error_content = str(runtime_info.error)
341
- elif 'message' in runtime_info.raw and isinstance(runtime_info.raw['message'], str) and 'error' in runtime_info.raw['message'].lower(): error_content = runtime_info.raw['message']
 
342
  elif 'error' in runtime_info.raw: error_content = str(runtime_info.raw['error'])
343
-
 
344
  if 'build' in runtime_info.raw and isinstance(runtime_info.raw['build'], dict) and runtime_info.raw['build'].get('status') == 'error':
345
- error_content = f"Build Error: {runtime_info.raw['build'].get('message', error_content or 'Unknown build error')}"
346
  elif 'run' in runtime_info.raw and isinstance(runtime_info.raw['run'], dict) and runtime_info.raw['run'].get('status') == 'error':
347
  error_content = f"Runtime Error: {runtime_info.raw['run'].get('message', error_content or 'Unknown runtime error')}"
348
-
349
  status_details["error_message"] = error_content if error_content else "Space is in an errored state. Check logs for details."
350
 
351
  logger.info(f"Runtime status for {repo_id}: {status_details['stage']}")
352
  return status_details, None
 
353
  except HfHubHTTPError as e_http: # Catch specific HF HTTP errors
354
- logger.error(f"HTTP error fetching runtime status for {repo_id_for_error_logging}: {e_http}")
355
  error_message = str(e_http)
356
- if e_http.response is not None:
357
- if e_http.response.status_code == 404:
358
- return None, f"Error: Space '{repo_id_for_error_logging}' not found or has no active runtime (404)."
359
- elif e_http.response.status_code in (401,403):
360
- return None, f"Error: Access denied or authentication required for '{repo_id_for_error_logging}' ({e_http.response.status_code})."
361
- return None, f"HTTP Error fetching runtime status for '{repo_id_for_error_logging}': {error_message}"
 
 
 
362
  except Exception as e:
363
- logger.exception(f"Error fetching runtime status for {repo_id_for_error_logging}:")
364
  return None, f"Error fetching runtime status: {str(e)}"
 
67
  in_code_block = False
68
 
69
  lines = markdown_input.strip().split("\n")
70
+
71
+ # Clean up potential leading '#' added by Gradio's Markdown sometimes
72
+ cleaned_lines = []
73
+ for line_content_orig in lines:
74
+ if line_content_orig.strip().startswith("# "):
75
+ # Only strip leading # if it looks like a Markdown heading related to our format
76
+ if line_content_orig.strip().startswith("# ### File:") or \
77
+ line_content_orig.strip().startswith("# ## File Structure") or \
78
+ line_content_orig.strip().startswith("# # Space:"):
79
+ cleaned_lines.append(line_content_orig.strip()[2:])
80
+ else:
81
+ cleaned_lines.append(line_content_orig)
82
+ else:
83
+ cleaned_lines.append(line_content_orig)
84
+
85
+ lines = cleaned_lines
86
+
87
 
88
  for line_content_orig in lines:
89
  line_content_stripped = line_content_orig.strip()
90
 
91
  if line_content_stripped.startswith("### File:"):
92
+ # Before processing a new file, save the content of the previous one
93
+ if current_file_path is not None and in_file_definition: # Check if we were inside a file definition
94
  space_info["files"].append({"path": current_file_path, "content": "\n".join(current_file_content_lines).strip()})
95
+
96
  current_file_path = line_content_stripped.replace("### File:", "").strip()
97
+ # Clean up potential trailing descriptions like "(main application)"
98
  current_file_path = re.split(r'\s*\(', current_file_path, 1)[0].strip()
99
+ # Clean up potential backticks around the filename
100
+ current_file_path = current_file_path.strip('`')
101
+
102
+
103
  current_file_content_lines = []
104
  in_file_definition = True
105
+ in_code_block = False # Reset code block flag for the new file
106
  continue
107
 
108
+ # If we are not currently inside a file definition block (i.e., before the first "### File:")
109
  if not in_file_definition:
110
  if line_content_stripped.startswith("# Space:"):
111
  full_space_name_md = line_content_stripped.replace("# Space:", "").strip()
112
  if "/" in full_space_name_md:
113
+ parts = full_space_name_md.split("/", 1)
114
+ if len(parts) == 2:
115
+ space_info["owner_md"], space_info["repo_name_md"] = parts[0].strip(), parts[1].strip()
116
+ else:
117
+ space_info["repo_name_md"] = full_space_name_md # Handle case like "user/repo/"
118
  else:
119
  space_info["repo_name_md"] = full_space_name_md
120
+ # Ignore other lines outside a file block for now (like "## File Structure" preamble)
121
  continue
122
 
123
+ # If we are inside a file definition block
124
  if in_file_definition:
125
  if line_content_stripped.startswith("```"):
126
+ # Toggle code block status
127
  in_code_block = not in_code_block
128
+ # If exiting a code block, the next lines are not part of the code
129
+ if not in_code_block:
130
+ # We consume the ``` line itself, don't add it to content
131
+ pass
132
+ else:
133
+ # If entering a code block, we consume the ```lang line itself
134
+ pass
135
+ continue # Do not add the ``` line to content
136
 
137
+ # If inside a code block, add the line as-is (original content, including leading/trailing whitespace)
138
  if in_code_block:
139
  current_file_content_lines.append(line_content_orig)
140
+ # If not inside a code block, check for binary file marker
141
+ elif line_content_stripped.startswith("[Binary file") or line_content_stripped.startswith("[Error loading content:") or line_content_stripped.startswith("[Binary or Skipped file]"):
142
+ # Handle binary file markers or error messages as content if not in code block
143
  current_file_content_lines.append(line_content_orig)
144
+ # Any other lines outside code blocks within a file definition are ignored (e.g., descriptions, blank lines)
145
+ # This assumes all code/content *must* be within ``` blocks or be a specific marker line.
146
 
147
+
148
+ # After the loop, save the content of the last file
149
+ if current_file_path is not None and in_file_definition:
150
  space_info["files"].append({"path": current_file_path, "content": "\n".join(current_file_content_lines).strip()})
151
 
152
+ # Ensure all file paths are valid and clean up empty files if necessary (based on content parsing)
153
+ # The parsing logic above should handle stripping content, but this is a final check
154
+ space_info["files"] = [f for f in space_info["files"] if f.get("path")] # Ensure path exists
155
+ # Optional: Filter out files where content became empty after strip() if that's desired behavior.
156
+ # Currently, it keeps files with empty content, which is fine for creating empty files.
157
+
158
+ # Clean up owner/repo names from potential whitespace
159
+ space_info["owner_md"] = space_info["owner_md"].strip()
160
+ space_info["repo_name_md"] = space_info["repo_name_md"].strip()
161
+
162
+
163
  return space_info
164
 
165
 
 
169
  sdk = None
170
  files = []
171
  error = None
172
+ repo_id = None # Define repo_id here to ensure it's available for error logging after _determine_repo_id
173
 
174
  try:
175
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
 
177
 
178
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
179
  if err_repo_id: return None, None, err_repo_id
180
+ repo_id_for_error_logging = repo_id # Update logging name
181
 
182
  api = HfApi(token=resolved_api_token)
183
+ # Use repo_info endpoint as it's more robust and gives SDK
184
+ repo_info_obj = api.repo_info(repo_id=repo_id, repo_type="space", timeout=10) # Added timeout
185
  sdk = repo_info_obj.sdk
186
  files = [sibling.rfilename for sibling in repo_info_obj.siblings if sibling.rfilename]
187
+
188
  if not files and repo_info_obj.siblings:
189
  logger.warning(f"Repo {repo_id} has siblings but no rfilenames extracted.")
190
+
191
  except HfHubHTTPError as e_http: # Catch specific HF HTTP errors first
192
+ logger.error(f"HTTP error getting repo info for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
193
  error_message = str(e_http)
194
+ status_code = e_http.response.status_code if e_http.response is not None else None
195
+
196
+ if status_code == 404:
197
+ error = f"Space '{repo_id_for_error_logging or 'unknown repo'}' not found (404)."
198
+ elif status_code in (401,403):
199
+ error = f"Access denied for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
200
+ else:
201
+ error = f"HTTP Error {status_code or 'unknown'} for '{repo_id_for_error_logging or 'unknown repo'}': {error_message}"
202
+
203
  except Exception as e: # Catch other general exceptions
204
+ # If repo_info failed, try listing files as a fallback
205
+ logger.warning(f"Could not get full repo_info for {repo_id_for_error_logging or 'unknown repo'}, attempting list_repo_files fallback: {e}")
206
+ error = f"Error retrieving Space info for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e)}. Attempting file list fallback." # Set a warning message
207
+
208
  try:
209
+ # Re-determine repo_id and get token for fallback
210
  resolved_api_token_fb, token_err_fb = _get_api_token(ui_api_token_from_textbox)
211
+ if token_err_fb: return None, None, f"{error}\nAPI Token Error during fallback: {token_err_fb}" # Propagate token error
212
  repo_id_fb, err_repo_id_fb = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
213
+ if err_repo_id_fb: return None, None, f"{error}\nRepo ID Error during fallback: {err_repo_id_fb}" # Propagate repo ID error
214
+
215
+ # Attempt to list files
216
+ files = list_repo_files(repo_id=repo_id_fb, token=resolved_api_token_fb, repo_type="space", timeout=10) # Added timeout
217
+ # If fallback is successful, update error message to a warning about repo_info
218
+ error = f"Warning: Could not fetch full Space info (SDK etc.) for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e)}. File list loaded via fallback."
219
+
220
  except HfHubHTTPError as e2_http:
221
+ logger.error(f"HTTP error during fallback list_repo_files for {repo_id_for_error_logging or 'unknown repo'}: {e2_http}")
222
  error_message_fb = str(e2_http)
223
+ status_code_fb = e2_http.response.status_code if e2_http.response is not None else None
224
+ if status_code_fb == 404:
225
+ error = f"Space '{repo_id_for_error_logging or 'unknown repo'}' not found during fallback (404)."
 
 
226
  else:
227
+ error = f"HTTP Error {status_code_fb or 'unknown'} for '{repo_id_for_error_logging or 'unknown repo'}' during fallback: {error_message_fb}"
228
+ files = [] # Ensure files list is empty on fallback error
229
+
230
  except Exception as e2:
231
+ logger.exception(f"Error listing files for {repo_id_for_error_logging or 'unknown repo'} during fallback: {e2}")
232
+ error = f"{error}\nError listing files during fallback for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e2)}"
233
+ files = [] # Ensure files list is empty on fallback error
234
 
235
 
236
+ # Final check: if files are still empty and there's no specific error, provide a generic "no files" message
237
  if not files and not error:
238
+ error = f"No files found in Space `{repo_id_for_error_logging or 'unknown repo'}` (or an issue fetching them)."
239
+
240
  return sdk, files, error
241
 
242
 
 
249
  # --- Function to Fetch File Content from Hub ---
250
  def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo):
251
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
252
+ repo_id = None
253
  try:
254
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
255
  if token_err: return None, token_err
 
257
  if err_repo_id: return None, err_repo_id
258
  repo_id_for_error_logging = repo_id
259
  if not file_path_in_repo: return None, "Error: File path cannot be empty."
260
+ # Ensure file_path_in_repo uses forward slashes
261
+ file_path_in_repo = file_path_in_repo.replace("\\", "/")
262
+
263
+ # Use hf_hub_download first, which caches locally
264
+ downloaded_file_path = hf_hub_download(
265
+ repo_id=repo_id,
266
+ filename=file_path_in_repo,
267
+ repo_type="space",
268
+ token=resolved_api_token,
269
+ local_dir_use_symlinks=False, # Avoid symlinks issues
270
+ cache_dir=None # Use default cache dir
271
+ )
272
  content = Path(downloaded_file_path).read_text(encoding="utf-8")
273
  return content, None
274
+ except FileNotFoundError:
275
+ return None, f"Error: File '{file_path_in_repo}' not found locally after download attempt."
276
+ except UnicodeDecodeError:
277
+ # If read_text fails, it's likely binary or non-utf8 text
278
+ return None, f"Error: File '{file_path_in_repo}' is not valid UTF-8 text. Cannot display."
279
  except HfHubHTTPError as e_http:
280
+ logger.error(f"HTTP error fetching file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
281
  error_message = str(e_http)
282
+ status_code = e_http.response.status_code if e_http.response is not None else None
283
+ if status_code == 404:
284
+ return None, f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging or 'unknown repo'}' (404)."
285
+ if status_code in (401, 403):
286
+ return None, f"Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
287
  return None, f"HTTP Error fetching file '{file_path_in_repo}': {error_message}"
288
  except Exception as e:
289
+ logger.exception(f"Error fetching file content for {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}:")
290
  return None, f"Error fetching file content: {str(e)}"
291
 
292
  # --- Create/Update Space ---
293
  def create_space(ui_api_token_from_textbox, space_name_ui, owner_ui, sdk_ui, markdown_input):
294
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
295
+ repo_id = None
296
  try:
297
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
298
  if token_err: return token_err
299
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
300
  if err_repo_id: return err_repo_id
301
+ repo_id_for_error_logging = repo_id # Update logging name
302
+
303
  space_info = parse_markdown(markdown_input)
304
 
305
  with tempfile.TemporaryDirectory() as temp_dir:
306
  repo_staging_path = Path(temp_dir) / "repo_staging_content"
307
  repo_staging_path.mkdir(exist_ok=True)
308
+
309
+ # Always write .gitattributes to ensure LF line endings
310
+ gitattributes_path = repo_staging_path / ".gitattributes"
311
+ with open(gitattributes_path, "w") as f:
312
+ f.write("* text=auto eol=lf\n")
313
+
314
+ # If there are no files parsed from markdown *other than* the structure block,
315
+ # ensure the .gitattributes file is still staged.
316
+ if not [f for f in space_info["files"] if not f.get("is_structure_block")]:
317
+ logger.info(f"Markdown contained no standard files. Staging only .gitattributes for {repo_id}.")
318
+
319
+
320
  for file_info in space_info["files"]:
321
+ if not file_info.get("path") or file_info.get("is_structure_block"):
322
+ # Skip entries without a path or the structure block representation
323
+ if not file_info.get("path"): logger.warning(f"Skipping file_info with no path: {file_info}")
324
  continue
325
+
326
+ # Skip files that were marked as binary/error during loading
327
  content_to_write = file_info.get("content", "")
328
+ if content_to_write.startswith("[Binary file") or content_to_write.startswith("[Error loading content:") or content_to_write.startswith("[Binary or Skipped file]"):
329
+ logger.info(f"Skipping binary/error placeholder file from build: {file_info['path']}")
330
+ continue
331
+
332
+
333
+ file_path_abs = repo_staging_path / file_info["path"]
334
+ file_path_abs.parent.mkdir(parents=True, exist_ok=True) # Create parent directories
335
+ try:
336
+ # Ensure content is treated as text and written with utf-8 encoding
337
+ with open(file_path_abs, "w", encoding="utf-8") as f:
338
+ f.write(content_to_write)
339
+ except Exception as file_write_error:
340
+ logger.error(f"Error writing file {file_info['path']} during staging: {file_write_error}")
341
+ return f"Error staging file {file_info['path']}: {file_write_error}"
342
+
343
+
344
+ # Create or ensure repo exists
345
  create_repo(repo_id=repo_id, token=resolved_api_token, repo_type="space", space_sdk=sdk_ui, private=False, exist_ok=True)
346
+
347
+ api = HfApi(token=resolved_api_token)
348
+
349
+ # Determine files to delete (files on Hub not in markdown)
350
  try:
 
351
  current_hub_files_info = api.list_repo_files(repo_id=repo_id, repo_type="space", recursive=True)
352
  current_hub_files = set(current_hub_files_info)
353
+ # Get filenames from the markdown that were actually staged (not skipped binaries/structure)
354
+ markdown_staged_filenames = set(str(Path(temp_dir) / "repo_staging_content" / f.get("path")).relative_to(repo_staging_path) for f in space_info["files"] if f.get("path") and not f.get("is_structure_block") and not (f.get("content", "").startswith("[Binary file") or f.get("content", "").startswith("[Error loading content:") or f.get("content", "").startswith("[Binary or Skipped file]")))
355
+ markdown_staged_filenames.add(".gitattributes") # Always keep .gitattributes if we staged it
356
+
357
+ files_to_delete_on_hub = list(current_hub_files - markdown_staged_filenames)
358
+
359
+ # Exclude .git/ files and potentially README.md if we didn't explicitly include it in markdown
360
+ files_to_delete_on_hub = [f for f in files_to_delete_on_hub if not (f.startswith('.git') or (f == "README.md" and "README.md" not in markdown_staged_filenames))]
361
 
362
 
363
  if files_to_delete_on_hub:
364
+ logger.info(f"Deleting {len(files_to_delete_on_hub)} files from {repo_id} not in new markdown structure: {files_to_delete_on_hub}")
365
  delete_operations = [CommitOperationDelete(path_in_repo=f) for f in files_to_delete_on_hub]
366
+ if delete_operations:
367
+ # Check if there are also files to upload in this commit
368
+ if list(repo_staging_path.iterdir()): # Check if staging dir has anything to upload
369
+ # Combine delete and upload if possible (advanced scenario, requires specific hf_api methods)
370
+ # For simplicity here, do deletes in a separate commit before upload_folder
371
+ try:
372
+ api.create_commit(
373
+ repo_id=repo_id,
374
+ repo_type="space",
375
+ operations=delete_operations,
376
+ commit_message=f"AI Space Builder: Removed {len(files_to_delete_on_hub)} files not in updated structure."
377
+ )
378
+ logger.info("Successfully committed deletions.")
379
+ except Exception as e_delete_commit:
380
+ logger.error(f"Error committing deletions in {repo_id}: {e_delete_commit}. Proceeding with upload.")
381
+ # If delete commit fails, maybe upload_folder can handle concurrent ops?
382
+ # Or perhaps the files will be overwritten anyway if present in staging?
383
+ # It's safest to report the delete error but attempt upload.
384
+ else:
385
+ # If only deletions are happening (staging is empty except maybe .gitattributes)
386
+ try:
387
+ api.create_commit(
388
+ repo_id=repo_id,
389
+ repo_type="space",
390
+ operations=delete_operations,
391
+ commit_message=f"AI Space Builder: Removed {len(files_to_delete_on_hub)} files."
392
+ )
393
+ logger.info("Successfully committed deletions (only deletions).")
394
+ # If only deleting, we are done.
395
+ return f"Successfully updated Space: [{repo_id}](https://huggingface.co/spaces/{repo_id}) (Files deleted)."
396
+ except Exception as e_only_delete_commit:
397
+ logger.error(f"Error committing deletions (only deletions) in {repo_id}: {e_only_delete_commit}.")
398
+ return f"Error during Space update (deletions only): {str(e_only_delete_commit)}"
399
+
400
+
401
+ except Exception as e_delete_old_prep:
402
+ logger.error(f"Error during preparation for deletion of old files in {repo_id}: {e_delete_old_prep}. Proceeding with upload.")
403
+ # Don't return here, allow the upload to happen.
404
+
405
+
406
+ # Upload the staged files (including .gitattributes and any new/updated files)
407
+ logger.info(f"Uploading staged files from {str(repo_staging_path)} to {repo_id}")
408
+ # Use upload_folder which handles creating/updating files based on the staging directory content
409
+ upload_folder(
410
+ repo_id=repo_id,
411
+ folder_path=str(repo_staging_path),
412
+ path_in_repo=".", # Upload to the root of the repository
413
+ token=resolved_api_token,
414
+ repo_type="space",
415
+ commit_message=f"AI Space Builder: Space content update for {repo_id}"
416
+ )
417
+
418
  return f"Successfully created/updated Space: [{repo_id}](https://huggingface.co/spaces/{repo_id})"
419
+
420
+ except HfHubHTTPError as e_http:
421
+ logger.error(f"HTTP error during create_space for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
422
+ error_message = str(e_http)
423
+ status_code = e_http.response.status_code if e_http.response is not None else None
424
+ if status_code == 409: # Conflict, often means repo exists but maybe wrong type/owner?
425
+ return f"Error creating/updating Space '{repo_id_for_error_logging or 'unknown repo'}': Conflict (Space might exist with different owner/settings)."
426
+ if status_code in (401, 403):
427
+ return f"Error creating/updating Space '{repo_id_for_error_logging or 'unknown repo'}': Access denied or authentication required ({status_code}). Check token permissions."
428
+ return f"HTTP Error {status_code or 'unknown'} during Space creation/update: {error_message}"
429
  except Exception as e:
430
+ logger.exception(f"Error in create_space for {repo_id_for_error_logging or 'unknown repo'}:")
431
  return f"Error during Space creation/update: {str(e)}"
432
 
433
  # --- Update Single File ---
434
  def update_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, file_content, commit_message_ui):
435
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
436
+ repo_id = None
437
  try:
438
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
439
  if token_err: return token_err
440
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
441
  if err_repo_id: return err_repo_id
442
+ repo_id_for_error_logging = repo_id # Update logging name
443
+
444
  if not file_path_in_repo: return "Error: File Path to update cannot be empty."
445
+ file_path_in_repo = file_path_in_repo.lstrip('/').replace(os.sep, '/') # Clean path for Hub
446
  commit_msg = commit_message_ui or f"Update {file_path_in_repo} via AI Space Editor"
447
+
448
  api = HfApi(token=resolved_api_token)
449
+
450
+ # Use a temporary file to upload content safely
451
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as tmp_file_obj:
452
+ tmp_file_obj.write(file_content)
453
  tmp_file_path = tmp_file_obj.name
454
+
455
  try:
456
+ # Upload the temporary file to the specified path in the repo
457
+ api.upload_file(
458
+ path_or_fileobj=tmp_file_path,
459
+ path_in_repo=file_path_in_repo,
460
+ repo_id=repo_id,
461
+ repo_type="space",
462
+ commit_message=commit_msg
463
+ )
464
  return f"Successfully updated `{file_path_in_repo}` in Space [{repo_id}](https://huggingface.co/spaces/{repo_id})"
465
  finally:
466
+ # Ensure the temporary file is removed
467
+ if os.path.exists(tmp_file_path):
468
+ os.remove(tmp_file_path)
469
+
470
+ except FileNotFoundError:
471
+ return f"Error: Local temporary file not found during upload for '{file_path_in_repo}'."
472
+ except HfHubHTTPError as e_http:
473
+ logger.error(f"HTTP error in update_space_file for {repo_id_for_error_logging or 'unknown repo'}, file {file_path_in_repo}: {e_http}")
474
+ error_message = str(e_http)
475
+ status_code = e_http.response.status_code if e_http.response is not None else None
476
+ if status_code == 404:
477
+ return f"Error: Space '{repo_id_for_error_logging or 'unknown repo'}' or file '{file_path_in_repo}' not found (404)."
478
+ if status_code in (401, 403):
479
+ return f"Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
480
+ return f"HTTP Error {status_code or 'unknown'} updating file '{file_path_in_repo}': {error_message}"
481
  except Exception as e:
482
+ logger.exception(f"Error in update_space_file for {repo_id_for_error_logging or 'unknown repo'}, file {file_path_in_repo}:")
483
+ return f"Error updating file for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e)}"
484
+
485
 
486
  # --- Delete Single File ---
487
  def delete_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, commit_message_ui=None):
488
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
489
+ repo_id = None
490
  try:
491
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
492
  if token_err: return f"API Token Error: {token_err}"
493
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
494
  if err_repo_id: return f"Repo ID Error: {err_repo_id}"
495
+ repo_id_for_error_logging = repo_id # Update logging name
496
+
497
  if not file_path_in_repo: return "Error: File path cannot be empty for deletion."
498
+ file_path_in_repo = file_path_in_repo.lstrip('/').replace(os.sep, '/') # Clean path for Hub
499
+
500
+ # Prevent deleting essential files like .gitattributes or README.md unless explicitly handled?
501
+ # For now, allow deleting anything selected in the dropdown.
502
+
503
  effective_commit_message = commit_message_ui or f"Deleted file: {file_path_in_repo} via AI Space Editor"
504
+
505
+ # Use hf_delete_file directly
506
+ hf_delete_file(
507
+ path_in_repo=file_path_in_repo,
508
+ repo_id=repo_id,
509
+ repo_type="space",
510
+ token=resolved_api_token,
511
+ commit_message=effective_commit_message
512
+ )
513
  return f"Successfully deleted file: {file_path_in_repo}"
514
+
515
  except HfHubHTTPError as e_http: # Catch specific HF HTTP errors
516
+ logger.error(f"HTTP error deleting file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
517
  error_message = str(e_http)
518
+ status_code = e_http.response.status_code if e_http.response is not None else None
519
+
520
+ if status_code == 404:
521
+ return f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging or 'unknown repo'}' for deletion (404)."
522
+ if status_code in (401, 403):
523
+ return f"Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
524
+ return f"HTTP Error {status_code or 'unknown'} deleting file '{file_path_in_repo}': {error_message}"
525
  except Exception as e:
526
+ logger.exception(f"Error deleting file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}:")
527
  return f"Error deleting file '{file_path_in_repo}': {str(e)}"
528
 
529
  # --- Get Space Runtime Status ---
530
  def get_space_runtime_status(ui_api_token_from_textbox, space_name_ui, owner_ui):
531
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
532
+ repo_id = None
533
  try:
534
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
535
  if token_err: return None, f"API Token Error: {token_err}"
536
  repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
537
  if err_repo_id: return None, f"Repo ID Error: {err_repo_id}"
538
+ repo_id_for_error_logging = repo_id # Update logging name
539
+
540
  api = HfApi(token=resolved_api_token)
541
  logger.info(f"Fetching runtime status for Space: {repo_id}")
542
+
543
+ # Use get_space_runtime which provides details like stage, hardware, etc.
544
  runtime_info = api.get_space_runtime(repo_id=repo_id)
545
+
546
+ # Structure the details for display
547
  status_details = {
548
  "stage": runtime_info.stage,
549
  "hardware": runtime_info.hardware,
550
+ "requested_hardware": runtime_info.requested_hardware if hasattr(runtime_info, 'requested_hardware') else None, # requested_hardware might not always be present
551
+ "error_message": None,
552
  "full_log_link": f"https://huggingface.co/spaces/{repo_id}/logs",
553
+ "raw_data": runtime_info.raw # Include raw data for detailed inspection if needed
554
  }
555
+
556
+ # Check for specific error states or messages
557
  if runtime_info.stage == "ERRORED":
558
  error_content = None
559
+ # Look for error details in various places within the raw data or the error attribute
560
  if hasattr(runtime_info, 'error') and runtime_info.error: error_content = str(runtime_info.error)
561
+ elif 'message' in runtime_info.raw and isinstance(runtime_info.raw['message'], str) and ('error' in runtime_info.raw['message'].lower() or runtime_info.raw['message'].strip().endswith('!')): # Basic check for message indicative of error
562
+ error_content = runtime_info.raw['message']
563
  elif 'error' in runtime_info.raw: error_content = str(runtime_info.raw['error'])
564
+
565
+ # Check build/run specific error messages in raw data
566
  if 'build' in runtime_info.raw and isinstance(runtime_info.raw['build'], dict) and runtime_info.raw['build'].get('status') == 'error':
567
+ error_content = f"Build Error: {runtime_info.raw['build'].get('message', error_content or 'Unknown build error')}"
568
  elif 'run' in runtime_info.raw and isinstance(runtime_info.raw['run'], dict) and runtime_info.raw['run'].get('status') == 'error':
569
  error_content = f"Runtime Error: {runtime_info.raw['run'].get('message', error_content or 'Unknown runtime error')}"
570
+
571
  status_details["error_message"] = error_content if error_content else "Space is in an errored state. Check logs for details."
572
 
573
  logger.info(f"Runtime status for {repo_id}: {status_details['stage']}")
574
  return status_details, None
575
+
576
  except HfHubHTTPError as e_http: # Catch specific HF HTTP errors
577
+ logger.error(f"HTTP error fetching runtime status for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
578
  error_message = str(e_http)
579
+ status_code = e_http.response.status_code if e_http.response is not None else None
580
+
581
+ if status_code == 404:
582
+ # A 404 could mean the space doesn't exist or doesn't have an active runtime state recorded
583
+ return None, f"Error: Space '{repo_id_for_error_logging or 'unknown repo'}' not found or has no active runtime status (404)."
584
+ if status_code in (401, 403):
585
+ return None, f"Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
586
+ return None, f"HTTP Error {status_code or 'unknown'} fetching runtime status for '{repo_id_for_error_logging or 'unknown repo'}': {error_message}"
587
+
588
  except Exception as e:
589
+ logger.exception(f"Error fetching runtime status for {repo_id_for_error_logging or 'unknown repo'}:")
590
  return None, f"Error fetching runtime status: {str(e)}"