broadfield-dev commited on
Commit
3103a1e
·
verified ·
1 Parent(s): b07de5c

Update build_logic.py

Browse files
Files changed (1) hide show
  1. build_logic.py +144 -291
build_logic.py CHANGED
@@ -15,30 +15,26 @@ from huggingface_hub import (
15
  HfApi
16
  )
17
  from huggingface_hub.hf_api import CommitOperationDelete, CommitOperationAdd, CommitOperation
18
- # Import the general HTTP error from huggingface_hub.utils
19
- from huggingface_hub.utils import HfHubHTTPError # For catching specific HF HTTP errors
20
 
21
- # Setup basic logging
22
  logging.basicConfig(
23
  level=logging.INFO,
24
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
25
  )
26
  logger = logging.getLogger(__name__)
27
 
28
- # --- Helper Function to Get API Token ---
29
  def _get_api_token(ui_token_from_textbox=None):
30
  env_token = os.getenv('HF_TOKEN')
31
  if env_token:
32
- logger.info("Using HF_TOKEN from environment variable.")
33
  return env_token, None
34
  if ui_token_from_textbox:
35
- logger.info("Using HF_TOKEN from UI textbox.")
36
  return ui_token_from_textbox.strip(), None
37
  logger.warning("Hugging Face API token not provided in UI or HF_TOKEN env var.")
38
  return None, "Error: Hugging Face API token not provided in UI or HF_TOKEN env var."
39
 
40
- # --- Helper Function to Determine Repo ID ---
41
- def _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui):
42
  if not space_name_ui: return None, "Error: Space Name cannot be empty."
43
  if "/" in space_name_ui: return None, "Error: Space Name should not contain '/'. Use Owner field for the owner part."
44
 
@@ -48,7 +44,7 @@ def _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui):
48
  if not final_owner:
49
  logger.info("Owner not specified, attempting to auto-detect from token.")
50
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
51
- if token_err: return None, token_err
52
  if not resolved_api_token: return None, "Error: API token required for auto owner determination if Owner field is empty."
53
  try:
54
  user_info = whoami(token=resolved_api_token)
@@ -68,26 +64,19 @@ def _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui):
68
  logger.info(f"Determined repo_id: {repo_id}")
69
  return repo_id, None
70
 
71
-
72
- # --- Corrected Markdown Parsing ---
73
- # This function remains mostly the same as its purpose is just to parse the *AI's output format*
74
- # into a structured format, not necessarily to represent the *current state* of a Space.
75
- # The app.py logic will use this output and combine it with the current Space state.
76
  def parse_markdown(markdown_input):
77
  space_info = {"repo_name_md": "", "owner_md": "", "files": []}
78
  current_file_path = None
79
  current_file_content_lines = []
80
  in_file_definition = False
81
  in_code_block = False
82
- file_parsing_errors = [] # To collect potential parsing issues
83
 
84
  lines = markdown_input.strip().split("\n")
85
 
86
- # Clean up potential leading '#' added by Gradio's Markdown sometimes
87
  cleaned_lines = []
88
  for line_content_orig in lines:
89
  if line_content_orig.strip().startswith("# "):
90
- # Only strip leading # if it looks like a Markdown heading related to our format
91
  if line_content_orig.strip().startswith("# ### File:") or \
92
  line_content_orig.strip().startswith("# ## File Structure") or \
93
  line_content_orig.strip().startswith("# # Space:"):
@@ -104,35 +93,29 @@ def parse_markdown(markdown_input):
104
  line_content_stripped = line_content_orig.strip()
105
  line_num = i + 1
106
 
107
- # Check for file header
108
  file_match = re.match(r"### File:\s*(?P<filename_line>[^\n]+)", line_content_stripped)
109
  if file_match:
110
- # Before processing a new file, save the content of the previous one
111
- if current_file_path is not None and in_file_definition: # Check if we were inside a file definition
112
- # Remove leading/trailing blank lines from content lines
113
  content_to_save = "\n".join(current_file_content_lines).strip()
114
  space_info["files"].append({"path": current_file_path, "content": content_to_save})
115
 
116
  filename_line = file_match.group("filename_line").strip()
117
  current_file_path = filename_line
118
- # Clean up potential trailing descriptions like "(main application)"
119
  current_file_path = re.split(r'\s*\(', current_file_path, 1)[0].strip()
120
- # Clean up potential backticks around the filename
121
- current_file_path = current_file_path.strip('`\'"').strip() # Add more chars to strip
122
 
123
  if not current_file_path:
124
  file_parsing_errors.append(f"Line {line_num}: Found '### File:' but filename is empty or invalid.")
125
- current_file_path = None # Invalidate current file path if parsing failed
126
- in_file_definition = False # Exit file definition mode until a valid one is found
127
- continue # Move to next line
128
 
129
  current_file_content_lines = []
130
  in_file_definition = True
131
- in_code_block = False # Reset code block flag for the new file
132
  logger.debug(f"Parsed file header: {current_file_path}")
133
- continue # Move to next line
134
 
135
- # If not a file header, check for other top-level structures *before* file definitions start
136
  if not in_file_definition:
137
  if line_content_stripped.startswith("# Space:"):
138
  full_space_name_md = line_content_stripped.replace("# Space:", "").strip()
@@ -141,86 +124,59 @@ def parse_markdown(markdown_input):
141
  if len(parts) == 2:
142
  space_info["owner_md"], space_info["repo_name_md"] = parts[0].strip(), parts[1].strip()
143
  else:
144
- space_info["repo_name_md"] = full_space_name_md # Handle case like "user/repo/"
145
  else:
146
  space_info["repo_name_md"] = full_space_name_md
147
  logger.debug(f"Parsed space header: {space_info['owner_md']}/{space_info['repo_name_md']}")
148
  continue
149
- # Ignore ## File Structure headers and their code blocks, as they are not file content
150
  if line_content_stripped.startswith("## File Structure"):
151
- # Need to consume the following code block if it exists
152
  structure_block_start = i + 1
153
  while structure_block_start < len(lines) and not lines[structure_block_start].strip().startswith("```"):
154
  structure_block_start += 1
155
  if structure_block_start < len(lines) and lines[structure_block_start].strip().startswith("```"):
156
- # Found opening ```, look for closing ```
157
  structure_block_end = structure_block_start + 1
158
  while structure_block_end < len(lines) and not lines[structure_block_end].strip().startswith("```"):
159
  structure_block_end += 1
160
  if structure_block_end < len(lines) and lines[structure_block_end].strip().startswith("```"):
161
- # Found closing ```, skip all these lines
162
  logger.debug(f"Skipping File Structure block from line {i+1} to {structure_block_end+1}")
163
- i = structure_block_end # Adjust loop counter (outer loop will increment)
164
  continue
165
- # Ignore other lines outside a file block definition
166
  continue
167
 
168
- # If we are inside a file definition block (in_file_definition is True)
169
  if in_file_definition:
170
- # Check for code block start/end
171
  if line_content_stripped.startswith("```"):
172
- # Toggle code block status
173
  in_code_block = not in_code_block
174
- # We consume the ``` line(s), do not add to content
175
  logger.debug(f"Toggled code block to {in_code_block} at line {line_num}")
176
- continue # Do not add the ``` line to content
177
 
178
- # If inside a code block, add the line as-is (original content, including leading/trailing whitespace)
179
  if in_code_block:
180
  current_file_content_lines.append(line_content_orig)
181
- # If not inside a code block, check for binary file marker or error messages
182
  elif line_content_stripped.startswith("[Binary file") or line_content_stripped.startswith("[Error loading content:") or line_content_stripped.startswith("[Binary or Skipped file]"):
183
- # Handle binary file markers or error messages as content if not in code block
184
  current_file_content_lines.append(line_content_orig)
185
  logger.debug(f"Parsed binary/error marker for {current_file_path} at line {line_num}")
186
- # Any other lines outside code blocks within a file definition are ignored (e.g., descriptions, blank lines)
187
- # This assumes all code/content *must* be within ``` blocks or be a specific marker line.
188
  else:
189
- # Optionally log ignored lines within a file block if debugging parsing
190
- # logger.debug(f"Ignoring line {line_num} within file {current_file_path}: '{line_content_orig}'")
191
  pass
192
 
193
-
194
- # After the loop, save the content of the last file if we were inside a file definition
195
  if current_file_path is not None and in_file_definition:
196
  content_to_save = "\n".join(current_file_content_lines).strip()
197
  space_info["files"].append({"path": current_file_path, "content": content_to_save})
198
 
199
-
200
- # Ensure all file paths are valid and clean up empty files if necessary (based on content parsing)
201
- # The parsing logic above should handle stripping content, but this is a final check
202
- space_info["files"] = [f for f in space_info["files"] if f.get("path")] # Ensure path exists
203
-
204
- # Clean up owner/repo names from potential whitespace
205
  space_info["owner_md"] = space_info["owner_md"].strip()
206
  space_info["repo_name_md"] = space_info["repo_name_md"].strip()
207
 
208
  if file_parsing_errors:
209
  logger.warning(f"Markdown parsing encountered errors: {file_parsing_errors}")
210
- # You might want to return the errors or include them in the space_info dict
211
- # For now, we just log them.
212
 
213
  logger.info(f"Parsed markdown. Found {len(space_info['files'])} files.")
214
  return space_info
215
 
216
-
217
- # --- Function to Get Space SDK and Files ---
218
  def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui):
219
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
220
  sdk = None
221
  files = []
222
  error = None
223
- repo_id = None # Define repo_id here to ensure it's available for error logging after _determine_repo_id
224
 
225
  logger.info(f"Attempting to get repo info for {repo_id_for_error_logging}")
226
 
@@ -228,25 +184,22 @@ def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui
228
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
229
  if token_err: return None, [], token_err
230
 
231
- repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
232
  if err_repo_id: return None, [], err_repo_id
233
- repo_id_for_error_logging = repo_id # Update logging name
234
 
235
  api = HfApi(token=resolved_api_token)
236
- # Use repo_info endpoint as it's more robust and gives SDK
237
- repo_info_obj = api.repo_info(repo_id=repo_id, repo_type="space", timeout=20) # Added timeout, increased slightly
238
  sdk = repo_info_obj.sdk
239
  files = [sibling.rfilename for sibling in repo_info_obj.siblings if sibling.rfilename]
240
 
241
  if not files and repo_info_obj.siblings:
242
  logger.warning(f"Repo {repo_id} has siblings but no rfilenames extracted. Total siblings: {len(repo_info_obj.siblings)}")
243
- # Sometimes empty repos exist, or listing might fail partially.
244
- # Continue, files list is just empty.
245
 
246
  logger.info(f"Successfully got repo info for {repo_id}. SDK: {sdk}, Files found: {len(files)}")
247
 
248
 
249
- except HfHubHTTPError as e_http: # Catch specific HF HTTP errors first
250
  logger.error(f"HTTP error getting repo info for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
251
  error_message = str(e_http)
252
  status_code = e_http.response.status_code if e_http.response is not None else None
@@ -258,21 +211,17 @@ def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui
258
  else:
259
  error = f"HTTP Error {status_code or 'unknown'} for '{repo_id_for_error_logging or 'unknown repo'}': {error_message}"
260
 
261
- except Exception as e: # Catch other general exceptions
262
- # If repo_info failed, try listing files as a fallback
263
  logger.warning(f"Could not get full repo_info for {repo_id_for_error_logging or 'unknown repo'}, attempting list_repo_files fallback: {e}")
264
- 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
265
 
266
  try:
267
- # Re-determine repo_id and get token for fallback
268
  resolved_api_token_fb, token_err_fb = _get_api_token(ui_api_token_from_textbox)
269
- if token_err_fb: return None, [], f"{error}\nAPI Token Error during fallback: {token_err_fb}" # Propagate token error
270
- repo_id_fb, err_repo_id_fb = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
271
- if err_repo_id_fb: return None, [], f"{error}\nRepo ID Error during fallback: {err_repo_id_fb}" # Propagate repo ID error
272
 
273
- # Attempt to list files
274
- files = list_repo_files(repo_id=repo_id_fb, token=resolved_api_token_fb, repo_type="space", timeout=20) # Added timeout
275
- # If fallback is successful, update error message to a warning about repo_info
276
  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."
277
  logger.info(f"Fallback list_repo_files successful for {repo_id_fb}. Files found: {len(files)}")
278
 
@@ -284,29 +233,22 @@ def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui
284
  error = f"Space '{repo_id_for_error_logging or 'unknown repo'}' not found during fallback (404)."
285
  else:
286
  error = f"HTTP Error {status_code_fb or 'unknown'} for '{repo_id_for_error_logging or 'unknown repo'}' during fallback: {error_message_fb}"
287
- files = [] # Ensure files list is empty on fallback error
288
 
289
  except Exception as e2:
290
  logger.exception(f"Error listing files for {repo_id_for_error_logging or 'unknown repo'} during fallback: {e2}")
291
  error = f"{error}\nError listing files during fallback for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e2)}"
292
- files = [] # Ensure files list is empty on fallback error
293
-
294
 
295
- # Final check: if files are still empty and there's no specific error, provide a generic "no files" message
296
- # or if a specific 404 error occurred.
297
  if not files and not error and (repo_id_for_error_logging is not None):
298
  error = f"No files found in Space `{repo_id_for_error_logging or 'unknown repo'}`."
299
 
300
  return sdk, files, error
301
 
302
-
303
- # --- Function to list files ---
304
  def list_space_files_for_browsing(ui_api_token_from_textbox, space_name_ui, owner_ui):
305
  files, err = get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui)[1:]
306
  return files, err
307
 
308
-
309
- # --- Function to Fetch File Content from Hub ---
310
  def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo):
311
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
312
  repo_id = None
@@ -314,32 +256,29 @@ def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, f
314
  try:
315
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
316
  if token_err: return None, token_err
317
- repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
318
  if err_repo_id: return None, err_repo_id
319
- repo_id_for_error_logging = repo_id # Update logging name
320
 
321
  if not file_path_in_repo: return None, "Error: File path cannot be empty."
322
- # Ensure file_path_in_repo uses forward slashes
323
  file_path_in_repo = file_path_in_repo.replace("\\", "/")
324
 
325
- # Use hf_hub_download first, which caches locally
326
  downloaded_file_path = hf_hub_download(
327
  repo_id=repo_id,
328
  filename=file_path_in_repo,
329
  repo_type="space",
330
  token=resolved_api_token,
331
- local_dir_use_symlinks=False, # Avoid symlinks issues
332
- cache_dir=None, # Use default cache dir
333
- timeout=20 # Added timeout
334
  )
335
  content = Path(downloaded_file_path).read_text(encoding="utf-8")
336
  logger.info(f"Successfully downloaded and read content for '{file_path_in_repo}'.")
337
  return content, None
338
  except FileNotFoundError:
339
  logger.error(f"FileNotFoundError for '{file_path_in_repo}' in {repo_id_for_error_logging or 'unknown'}")
340
- return None, f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging or 'unknown repo'}' (404)." # Often gets translated from 404 by hf_hub_download
341
  except UnicodeDecodeError:
342
- # If read_text fails, it's likely binary or non-utf8 text
343
  logger.warning(f"UnicodeDecodeError for '{file_path_in_repo}'. Likely binary.")
344
  return None, f"Error: File '{file_path_in_repo}' is not valid UTF-8 text. Cannot display."
345
  except HfHubHTTPError as e_http:
@@ -355,75 +294,53 @@ def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, f
355
  logger.exception(f"Error fetching file content for {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}:")
356
  return None, f"Error fetching file content: {str(e)}"
357
 
358
- # --- Create/Update Space from Staged Changes ---
359
- # This function is modified to take a list of operations (changeset) instead of markdown
360
- # It's designed to be called by handle_confirm_changes
361
  def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, changeset):
362
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
363
  repo_id = None
364
  status_messages = []
365
- operations = [] # List of CommitOperation objects
366
 
367
  logger.info(f"Attempting to apply {len(changeset)} staged changes to {repo_id_for_error_logging}")
368
 
369
  try:
370
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
371
- if token_err: return [token_err]
372
 
373
- repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, owner_ui, space_name_ui)
374
- if err_repo_id: return [err_repo_id]
375
  repo_id_for_error_logging = repo_id
376
 
377
  api = HfApi(token=resolved_api_token)
378
 
379
- # First, handle Space Creation if it's part of the changeset
380
  create_space_op = next((c for c in changeset if c['type'] == 'CREATE_SPACE'), None)
381
  if create_space_op:
382
- # The repo_id here should match the one determined above, derived from owner_ui/space_name_ui
383
- # We assume the UI fields owner_ui and space_name_ui are set to the *target* space for creation
384
- # This is a slight divergence from the AI's CREATE_SPACE command which specifies repo_id
385
- # We'll prioritize the UI fields as the user's explicit target.
386
- # The AI command should ideally set the UI fields first.
387
- # TODO: Refine AI CREATE_SPACE to update UI fields first? Or make build_logic_create_space use the AI's repo_id?
388
- # Let's adjust `create_space` to take the repo_id directly from the AI command, but validate/use UI token/owner if needed.
389
- # But the *current* structure assumes owner_ui/space_name_ui are the target.
390
- # Let's stick to the current structure for now: CREATE_SPACE action is noted, but the target repo is from UI fields.
391
- # The `create_space` call below *will* create the repo specified by UI fields.
392
- # The file operations will then be applied to this new repo.
393
-
394
- logger.info(f"Detected CREATE_SPACE action for {create_space_op['repo_id']}. Proceeding with creation of {repo_id_for_error_logging} based on UI fields.")
395
- # Call the existing create_space logic, but perhaps without markdown?
396
- # The subsequent file operations will populate it.
397
- # We need a way to create an *empty* repo first. HfApi().create_repo handles this.
398
  try:
399
  api.create_repo(repo_id=repo_id, repo_type="space", space_sdk=create_space_op.get('sdk', 'gradio'), private=create_space_op.get('private', False), exist_ok=True)
400
- status_messages.append(f"CREATE_SPACE: Successfully created or ensured space [{repo_id}](https://huggingface.co/spaces/{repo_id}) exists with SDK '{create_space_op.get('sdk', 'gradio')}' and private={create_space_op.get('private', False)}.")
401
  logger.info(f"Successfully created or ensured space {repo_id} exists.")
 
 
 
402
  except Exception as e:
403
  status_messages.append(f"CREATE_SPACE Error: {e}")
404
  logger.error(f"Error creating space {repo_id}: {e}")
405
- # If space creation fails, subsequent operations will also fail.
406
- # Should we stop here? Let's add a check.
407
- # We could try to proceed with file operations if `exist_ok=True` succeeded partially.
408
- # For simplicity, let's just report the error and continue, hoping upload_folder is resilient.
409
- # A better approach might stop if create_repo fails definitively (e.g., 401/403/409).
410
- pass # Continue attempting other operations
411
-
412
- # Prepare commit operations for file changes (Add/Update/Delete)
413
  temp_dir = None
414
- operations = [] # Reset operations list for the file commit
415
- paths_to_upload = {} # Map of local temp path -> path in repo for Add/Update
416
 
417
  try:
418
  temp_dir = tempfile.TemporaryDirectory()
419
  repo_staging_path = Path(temp_dir.name) / "repo_staging_content"
420
  repo_staging_path.mkdir(exist_ok=True)
421
 
422
- # Always stage .gitattributes to ensure consistent line endings
423
  gitattributes_path_local = repo_staging_path / ".gitattributes"
424
- with open(gitattributes_path_local, "w", encoding="utf-8") as f:
425
- f.write("* text=auto eol=lf\n")
426
- paths_to_upload[str(gitattributes_path_local)] = ".gitattributes"
 
 
 
 
427
 
428
 
429
  for change in changeset:
@@ -434,20 +351,19 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
434
  continue
435
 
436
  content_to_write = change.get('content', '')
437
- # Skip files that were marked as binary/error during loading
438
  if content_to_write.startswith("[Binary file") or content_to_write.startswith("[Error loading content:") or content_to_write.startswith("[Binary or Skipped file]"):
439
  status_messages.append(f"Skipping {change['type']} for '{file_path_in_repo}': Content is a binary/error placeholder.")
440
  logger.warning(f"Skipping {change['type']} operation for '{file_path_in_repo}': Content is binary/error placeholder.")
441
  continue
442
 
443
  file_path_local = repo_staging_path / file_path_in_repo
444
- file_path_local.parent.mkdir(parents=True, exist_ok=True) # Create parent directories
445
 
446
  try:
447
  with open(file_path_local, "w", encoding="utf-8") as f:
448
  f.write(content_to_write)
449
  paths_to_upload[str(file_path_local)] = file_path_in_repo
450
- logger.info(f"Staged file for {change['type']}: {file_path_in_repo}")
451
  except Exception as file_write_error:
452
  status_messages.append(f"Error staging file {file_path_in_repo} for {change['type']}: {file_write_error}")
453
  logger.error(f"Error writing file {file_path_in_repo} during staging for {change['type']}: {file_write_error}")
@@ -458,74 +374,51 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
458
  if not file_path_in_repo:
459
  status_messages.append(f"Skipping DELETE_FILE operation: empty path.")
460
  continue
461
- operations.append(CommitOperationDelete(path_in_repo=file_path_in_repo))
462
- logger.info(f"Added DELETE_FILE operation for: {file_path_in_repo}")
463
-
464
- # SET_PRIVACY and DELETE_SPACE are handled separately below after the commit
465
-
466
- # Add Add/Update operations from staged files
467
- for local_path, repo_path in paths_to_upload.items():
468
- # upload_folder/upload_file is simpler than CommitOperationAdd for content
469
- # Let's use upload_folder approach if there are files to upload
470
- pass # Just stage paths, upload_folder handles the ops
471
-
472
-
473
- # Perform the combined file commit (uploads and deletes)
474
- if paths_to_upload or operations: # Check if there's anything to commit
475
- logger.info(f"Committing file changes to {repo_id_for_error_logging}. Uploads: {len(paths_to_upload)}, Deletes: {len(operations)}")
476
-
477
- # upload_folder automatically handles Add/Update operations based on the local folder state
478
- # Deletes need to be handled via CommitOperations or a separate delete call.
479
- # The simplest approach might be:
480
- # 1. Perform deletes using create_commit with CommitOperationDelete.
481
- # 2. Perform adds/updates using upload_folder.
482
- # This requires two commits if there are both types of operations.
483
-
484
- delete_operations = [op for op in operations if isinstance(op, CommitOperationDelete)]
485
- if delete_operations:
486
- try:
487
- commit_message_delete = f"AI Space Builder: Deleted {len(delete_operations)} files."
488
- logger.info(f"Performing delete commit for {repo_id_for_error_logging}: {commit_message_delete}")
489
- api.create_commit(
490
- repo_id=repo_id,
491
- repo_type="space",
492
- operations=delete_operations,
493
- commit_message=commit_message_delete
494
- )
495
- status_messages.append(f"File Deletions: Successfully committed {len(delete_operations)} deletions.")
496
- logger.info("Delete commit successful.")
497
- except HfHubHTTPError as e_http:
498
- status_messages.append(f"File Deletion Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check logs.")
499
- logger.error(f"HTTP error during delete commit for {repo_id}: {e_http}")
500
- except Exception as e_delete_commit:
501
- status_messages.append(f"File Deletion Error: {str(e_delete_commit)}. Check logs.")
502
- logger.exception(f"Error during delete commit for {repo_id}:")
503
-
504
-
505
- if paths_to_upload: # If there are files to upload/update (including .gitattributes)
506
- try:
507
- commit_message_upload = f"AI Space Builder: Updated Space content for {repo_id}"
508
- logger.info(f"Uploading staged files from {str(repo_staging_path)} to {repo_id}...")
509
- upload_folder(
510
- repo_id=repo_id,
511
- folder_path=str(repo_staging_path),
512
- path_in_repo=".", # Upload to the root of the repository
513
- token=resolved_api_token,
514
- repo_type="space",
515
- commit_message=commit_message_upload,
516
- allow_patterns=["*"], # Ensure all staged files are considered
517
- # Use force_patterns to ensure specific files are uploaded even if git thinks they are binary
518
- # force_patterns=[f.get("path").replace("\\", "/") for f in space_info["files"] if f.get("path")] # This requires knowing the original markdown files
519
- # Let's rely on .gitattributes text=auto instead for now.
520
- )
521
- status_messages.append(f"File Uploads/Updates: Successfully uploaded/updated {len(paths_to_upload)} files.")
522
- logger.info("Upload/Update commit successful.")
523
- except HfHubHTTPError as e_http:
524
- status_messages.append(f"File Upload/Update Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check logs.")
525
- logger.error(f"HTTP error during upload_folder for {repo_id}: {e_http}")
526
- except Exception as e_upload:
527
- status_messages.append(f"File Upload/Update Error: {str(e_upload)}. Check logs.")
528
- logger.exception(f"Error during upload_folder for {repo_id}:")
529
 
530
  else:
531
  status_messages.append("No file changes (create/update/delete) to commit.")
@@ -533,7 +426,6 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
533
 
534
 
535
  finally:
536
- # Clean up temporary directory
537
  if temp_dir:
538
  try:
539
  temp_dir.cleanup()
@@ -541,12 +433,10 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
541
  except Exception as e:
542
  logger.error(f"Error cleaning up temp dir: {e}")
543
 
544
-
545
- # Handle Space Privacy and Delete actions *after* the file commit (if any)
546
  for change in changeset:
547
  if change['type'] == 'SET_PRIVACY':
548
  try:
549
- target_repo_id = change.get('repo_id', repo_id) # Use specified repo_id or current
550
  if not target_repo_id:
551
  status_messages.append("SET_PRIVACY Error: Target repo_id not specified.")
552
  continue
@@ -554,61 +444,49 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
554
  status_messages.append(f"SET_PRIVACY: Successfully set `{target_repo_id}` to `private={change['private']}`.")
555
  logger.info(f"Successfully set privacy for {target_repo_id} to {change['private']}.")
556
  except HfHubHTTPError as e_http:
557
- status_messages.append(f"SET_PRIVACY Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check token/permissions.")
558
  logger.error(f"HTTP error setting privacy for {target_repo_id}: {e_http}")
559
  except Exception as e:
560
  status_messages.append(f"SET_PRIVACY Error: {str(e)}. Check logs.")
561
  logger.exception(f"Error setting privacy for {target_repo_id}:")
562
 
563
  elif change['type'] == 'DELETE_SPACE':
564
- # This is destructive and typically requires the exact owner/space_name
565
- delete_owner = change.get('owner') or owner_ui # Use specified owner or current UI owner
566
- delete_space = change.get('space_name') or space_name_ui # Use specified space_name or current UI space name
567
  delete_repo_id = f"{delete_owner}/{delete_space}" if delete_owner and delete_space else repo_id
568
 
569
  if not delete_repo_id:
570
  status_messages.append("DELETE_SPACE Error: Target repo_id not specified.")
571
  continue
572
 
573
- # Add an extra safeguard: Only delete the *currently loaded* space unless AI specifies otherwise AND it matches the current UI fields?
574
- # Or strictly use the repo_id from the action? Let's strictly use the action's specified repo_id,
575
- # falling back to UI fields only if the action didn't provide them.
576
- # The action format is `DELETE_SPACE` (implies current UI space) or maybe `DELETE_SPACE owner/repo`?
577
- # The prompt defined `DELETE_SPACE` only, implying the current space. Let's enforce that.
578
- # The change object should have owner/space_name populated by generate_and_stage_changes based on current UI.
579
  if delete_repo_id != repo_id:
580
  status_messages.append(f"DELETE_SPACE Error: AI requested deletion of '{delete_repo_id}', but this action is only permitted for the currently loaded space '{repo_id}'. Action blocked.")
581
  logger.warning(f"Blocked DELETE_SPACE action: requested '{delete_repo_id}', current '{repo_id}'.")
582
- continue # Block deletion if not the currently loaded space
583
 
584
  logger.warning(f"Attempting DESTRUCTIVE DELETE_SPACE action for {delete_repo_id}")
585
  try:
586
  api.delete_repo(repo_id=delete_repo_id, repo_type='space')
587
  status_messages.append(f"DELETE_SPACE: Successfully deleted space `{delete_repo_id}`.")
588
- logger.info(f"Successfully deleted space {delete_repo_id}.")
589
  except HfHubHTTPError as e_http:
590
- status_messages.append(f"DELETE_SPACE Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check token/permissions.")
591
  logger.error(f"HTTP error deleting space {delete_repo_id}: {e_http}")
592
  except Exception as e:
593
  status_messages.append(f"DELETE_SPACE Error: {str(e)}. Check logs.")
594
  logger.exception(f"Error deleting space {delete_repo_id}:")
595
 
596
-
597
  except HfHubHTTPError as e_http:
598
  logger.error(f"Top-level HTTP error during apply_staged_changes for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
599
  status_messages.append(f"API HTTP Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}")
600
  except Exception as e:
601
  logger.exception(f"Top-level error during apply_staged_changes for {repo_id_for_error_logging or 'unknown repo'}:")
602
- status_messages.append(f"An unexpected error occurred: {str(e)}")
603
 
604
- # Format the final status message
605
  final_status = " | ".join(status_messages) if status_messages else "No operations were applied."
606
  logger.info(f"Finished applying staged changes. Final status: {final_status}")
607
  return final_status
608
 
609
-
610
- # --- Delete Single File (Manual UI Trigger) ---
611
- # This function remains for direct UI file deletion, distinct from the AI-driven workflow
612
  def delete_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, commit_message_ui=None):
613
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
614
  repo_id = None
@@ -616,111 +494,99 @@ def delete_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_p
616
  try:
617
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
618
  if token_err: return f"API Token Error: {token_err}"
619
- repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
620
  if err_repo_id: return f"Repo ID Error: {err_repo_id}"
621
- repo_id_for_error_logging = repo_id # Update logging name
622
 
623
  if not file_path_in_repo: return "Error: File path cannot be empty for deletion."
624
- file_path_in_repo = file_path_in_repo.lstrip('/').replace(os.sep, '/') # Clean path for Hub
625
-
626
- # Prevent deleting essential files like .gitattributes or README.md unless explicitly handled?
627
- # For now, allow deleting anything selected in the dropdown.
628
 
629
  effective_commit_message = commit_message_ui or f"Deleted file: {file_path_in_repo} via AI Space Editor UI"
630
 
631
- # Use hf_delete_file directly
632
  hf_delete_file(
633
  path_in_repo=file_path_in_repo,
634
  repo_id=repo_id,
635
  repo_type="space",
636
  token=resolved_api_token,
637
  commit_message=effective_commit_message,
638
- timeout=20 # Added timeout
639
  )
640
  logger.info(f"Successfully deleted file: {file_path_in_repo}")
641
  return f"Successfully deleted file: `{file_path_in_repo}`"
642
 
643
  except FileNotFoundError:
644
  logger.error(f"FileNotFoundError during manual delete for '{file_path_in_repo}' in {repo_id_for_error_logging or 'unknown'}")
645
- return f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging or 'unknown repo'}' (404)." # hf_delete_file translates 404
646
- except HfHubHTTPError as e_http: # Catch specific HF HTTP errors
647
  logger.error(f"HTTP error deleting file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
648
  error_message = str(e_http)
649
  status_code = e_http.response.status_code if e_http.response is not None else None
650
 
651
  if status_code == 404:
652
- return f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging or 'unknown repo'}' for deletion (404)."
653
  if status_code in (401, 403):
654
- return f"Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
655
- return f"HTTP Error {status_code or 'unknown'} deleting file '{file_path_in_repo}': {error_message}"
656
  except Exception as e:
657
  logger.exception(f"Error deleting file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}:")
658
- return f"Error deleting file '{file_path_in_repo}': {str(e)}"
659
 
660
- # --- Update Single File (Manual UI Trigger) ---
661
- # This function remains for direct UI file editing, distinct from the AI-driven workflow
662
  def update_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, file_content, commit_message_ui):
663
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
664
  repo_id = None
665
  logger.info(f"Attempting manual file update for '{file_path_in_repo}' in {repo_id_for_error_logging}")
666
  try:
667
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
668
- if token_err: return token_err
669
- repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
670
- if err_repo_id: return err_repo_id
671
- repo_id_for_error_logging = repo_id # Update logging name
672
 
673
- if not file_path_in_repo: return "Error: File Path to update cannot be empty."
674
- file_path_in_repo = file_path_in_repo.lstrip('/').replace(os.sep, '/') # Clean path for Hub
675
  commit_msg = commit_message_ui or f"Update {file_path_in_repo} via AI Space Editor UI"
676
 
677
  api = HfApi(token=resolved_api_token)
678
 
679
- # Use a temporary file to upload content safely
680
  tmp_file_path = None
681
  try:
682
  with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as tmp_file_obj:
683
  tmp_file_obj.write(file_content)
684
  tmp_file_path = tmp_file_obj.name
685
 
686
- # Upload the temporary file to the specified path in the repo
687
  api.upload_file(
688
  path_or_fileobj=tmp_file_path,
689
  path_in_repo=file_path_in_repo,
690
  repo_id=repo_id,
691
  repo_type="space",
692
  commit_message=commit_msg,
693
- timeout=20 # Added timeout
694
  )
695
  logger.info(f"Successfully updated file: {file_path_in_repo}")
696
  return f"Successfully updated `{file_path_in_repo}`"
697
  finally:
698
- # Ensure the temporary file is removed
699
  if tmp_file_path and os.path.exists(tmp_file_path):
700
  os.remove(tmp_file_path)
701
 
702
  except FileNotFoundError:
703
  logger.error(f"FileNotFoundError during manual update for '{file_path_in_repo}' in {repo_id_for_error_logging or 'unknown'}")
704
- return f"Error: Local temporary file not found during upload for '{file_path_in_repo}'."
705
  except UnicodeDecodeError:
706
- # If read_text fails, it's likely binary or non-utf8 text
707
  logger.warning(f"UnicodeDecodeError for '{file_path_in_repo}' during manual update.")
708
- return f"Error: Content for '{file_path_in_repo}' is not valid UTF-8 text. Cannot edit this way."
709
  except HfHubHTTPError as e_http:
710
  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}")
711
  error_message = str(e_http)
712
  status_code = e_http.response.status_code if e_http.response is not None else None
713
  if status_code == 404:
714
- return f"Error: Space '{repo_id_for_error_logging or 'unknown repo'}' or file '{file_path_in_repo}' not found (404)."
715
  if status_code in (401, 403):
716
- return f"Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
717
- return f"HTTP Error {status_code or 'unknown'} updating file '{file_path_in_repo}': {error_message}"
718
  except Exception as e:
719
  logger.exception(f"Error in update_space_file for {repo_id_for_error_logging or 'unknown repo'}, file {file_path_in_repo}:")
720
- return f"Error updating file for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e)}"
721
 
722
-
723
- # --- Get Space Runtime Status ---
724
  def get_space_runtime_status(ui_api_token_from_textbox, space_name_ui, owner_ui):
725
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
726
  repo_id = None
@@ -728,66 +594,55 @@ def get_space_runtime_status(ui_api_token_from_textbox, space_name_ui, owner_ui)
728
  try:
729
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
730
  if token_err: return None, f"API Token Error: {token_err}"
731
- repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
732
  if err_repo_id: return None, f"Repo ID Error: {err_repo_id}"
733
- repo_id_for_error_logging = repo_id # Update logging name
734
 
735
  api = HfApi(token=resolved_api_token)
736
 
737
- # Use get_space_runtime which provides details like stage, hardware, etc.
738
- # Added timeout for robustness
739
  runtime_info = api.get_space_runtime(repo_id=repo_id, timeout=20)
740
  logger.info(f"Received runtime info for {repo_id}. Stage: {runtime_info.stage}")
741
 
742
-
743
- # Structure the details for display
744
  status_details = {
745
  "stage": runtime_info.stage,
746
  "hardware": runtime_info.hardware,
747
- "requested_hardware": runtime_info.requested_hardware if hasattr(runtime_info, 'requested_hardware') else None, # requested_hardware might not always be present
748
- "error_message": None, # Populate this if stage is ERRORED
749
- "status": runtime_info.status if hasattr(runtime_info, 'status') else None, # Additional status field
750
  "full_log_link": f"https://huggingface.co/spaces/{repo_id}/logs" if repo_id else "#"
751
- # We can add more fields from runtime_info.raw if useful
752
  }
753
 
754
- # Check for specific error states or messages
755
  if runtime_info.stage == "ERRORED":
756
  error_content = None
757
- # Look for error details in various places within the raw data or the error attribute
758
  if hasattr(runtime_info, 'error') and runtime_info.error: error_content = str(runtime_info.error)
759
- # Check build/run specific error messages in raw data
760
  if 'build' in runtime_info.raw and isinstance(runtime_info.raw['build'], dict) and runtime_info.raw['build'].get('status') == 'error':
761
  error_content = f"Build Error: {runtime_info.raw['build'].get('message', error_content or 'Unknown build error')}"
762
  elif 'run' in runtime_info.raw and isinstance(runtime_info.raw['run'], dict) and runtime_info.raw['run'].get('status') == 'error':
763
  error_content = f"Runtime Error: {runtime_info.raw['run'].get('message', error_content or 'Unknown runtime error')}"
764
- 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
765
- error_content = runtime_info.raw['message'] # Prioritize messages from raw data if they look like errors
766
 
767
  status_details["error_message"] = error_content if error_content else "Space is in an errored state. Check logs for details."
768
 
769
  logger.info(f"Runtime status details for {repo_id}: {status_details}")
770
  return status_details, None
771
 
772
- except HfHubHTTPError as e_http: # Catch specific HF HTTP errors
773
  logger.error(f"HTTP error fetching runtime status for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
774
  error_message = str(e_http)
775
  status_code = e_http.response.status_code if e_http.response is not None else None
776
 
777
  if status_code == 404:
778
- # A 404 could mean the space doesn't exist or doesn't have an active runtime state recorded
779
- return None, f"Error: Space '{repo_id_for_error_logging or 'unknown repo'}' not found or has no active runtime status (404)."
780
  if status_code in (401, 403):
781
- return None, f"Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
782
- return None, f"HTTP Error {status_code or 'unknown'} fetching runtime status for '{repo_id_for_error_logging or 'unknown repo'}': {error_message}"
783
 
784
  except Exception as e:
785
  logger.exception(f"Error fetching runtime status for {repo_id_for_error_logging or 'unknown repo'}:")
786
- return None, f"Error fetching runtime status: {str(e)}"
787
 
788
- # --- Function to set space privacy ---
789
  def build_logic_set_space_privacy(hf_api_key, repo_id, private: bool):
790
- """Sets the privacy of a Hugging Face Space."""
791
  logger.info(f"Attempting to set privacy for '{repo_id}' to {private}.")
792
  try:
793
  token, err = _get_api_token(hf_api_key)
@@ -806,9 +661,7 @@ def build_logic_set_space_privacy(hf_api_key, repo_id, private: bool):
806
  logger.exception(f"Error setting privacy for {repo_id}:")
807
  return f"Error setting privacy for `{repo_id}`: {e}"
808
 
809
- # --- Function to delete an entire space ---
810
  def build_logic_delete_space(hf_api_key, owner, space_name):
811
- """Deletes an entire Hugging Face Space."""
812
  repo_id = f"{owner}/{space_name}"
813
  logger.warning(f"Attempting DESTRUCTIVE DELETE_SPACE action for '{repo_id}'.")
814
  try:
@@ -826,4 +679,4 @@ def build_logic_delete_space(hf_api_key, owner, space_name):
826
  return f"HTTP Error ({status_code}) deleting space `{repo_id}`: {e_http.response.text if e_http.response else str(e_http)}"
827
  except Exception as e:
828
  logger.exception(f"Error deleting space {repo_id}:")
829
- return f"Error deleting space `{repo_id}`: {e}"
 
15
  HfApi
16
  )
17
  from huggingface_hub.hf_api import CommitOperationDelete, CommitOperationAdd, CommitOperation
18
+ from huggingface_hub.utils import HfHubHTTPError
 
19
 
 
20
  logging.basicConfig(
21
  level=logging.INFO,
22
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
23
  )
24
  logger = logging.getLogger(__name__)
25
 
 
26
  def _get_api_token(ui_token_from_textbox=None):
27
  env_token = os.getenv('HF_TOKEN')
28
  if env_token:
29
+ logger.debug("Using HF_TOKEN from environment variable.")
30
  return env_token, None
31
  if ui_token_from_textbox:
32
+ logger.debug("Using HF_TOKEN from UI textbox.")
33
  return ui_token_from_textbox.strip(), None
34
  logger.warning("Hugging Face API token not provided in UI or HF_TOKEN env var.")
35
  return None, "Error: Hugging Face API token not provided in UI or HF_TOKEN env var."
36
 
37
+ def _determine_repo_id(ui_api_token_from_textbox, owner_ui, space_name_ui):
 
38
  if not space_name_ui: return None, "Error: Space Name cannot be empty."
39
  if "/" in space_name_ui: return None, "Error: Space Name should not contain '/'. Use Owner field for the owner part."
40
 
 
44
  if not final_owner:
45
  logger.info("Owner not specified, attempting to auto-detect from token.")
46
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
47
+ if token_err: return None, f"Error auto-detecting owner: {token_err}"
48
  if not resolved_api_token: return None, "Error: API token required for auto owner determination if Owner field is empty."
49
  try:
50
  user_info = whoami(token=resolved_api_token)
 
64
  logger.info(f"Determined repo_id: {repo_id}")
65
  return repo_id, None
66
 
 
 
 
 
 
67
  def parse_markdown(markdown_input):
68
  space_info = {"repo_name_md": "", "owner_md": "", "files": []}
69
  current_file_path = None
70
  current_file_content_lines = []
71
  in_file_definition = False
72
  in_code_block = False
73
+ file_parsing_errors = []
74
 
75
  lines = markdown_input.strip().split("\n")
76
 
 
77
  cleaned_lines = []
78
  for line_content_orig in lines:
79
  if line_content_orig.strip().startswith("# "):
 
80
  if line_content_orig.strip().startswith("# ### File:") or \
81
  line_content_orig.strip().startswith("# ## File Structure") or \
82
  line_content_orig.strip().startswith("# # Space:"):
 
93
  line_content_stripped = line_content_orig.strip()
94
  line_num = i + 1
95
 
 
96
  file_match = re.match(r"### File:\s*(?P<filename_line>[^\n]+)", line_content_stripped)
97
  if file_match:
98
+ if current_file_path is not None and in_file_definition:
 
 
99
  content_to_save = "\n".join(current_file_content_lines).strip()
100
  space_info["files"].append({"path": current_file_path, "content": content_to_save})
101
 
102
  filename_line = file_match.group("filename_line").strip()
103
  current_file_path = filename_line
 
104
  current_file_path = re.split(r'\s*\(', current_file_path, 1)[0].strip()
105
+ current_file_path = current_file_path.strip('`\'"').strip()
 
106
 
107
  if not current_file_path:
108
  file_parsing_errors.append(f"Line {line_num}: Found '### File:' but filename is empty or invalid.")
109
+ current_file_path = None
110
+ in_file_definition = False
111
+ continue
112
 
113
  current_file_content_lines = []
114
  in_file_definition = True
115
+ in_code_block = False
116
  logger.debug(f"Parsed file header: {current_file_path}")
117
+ continue
118
 
 
119
  if not in_file_definition:
120
  if line_content_stripped.startswith("# Space:"):
121
  full_space_name_md = line_content_stripped.replace("# Space:", "").strip()
 
124
  if len(parts) == 2:
125
  space_info["owner_md"], space_info["repo_name_md"] = parts[0].strip(), parts[1].strip()
126
  else:
127
+ space_info["repo_name_md"] = full_space_name_md
128
  else:
129
  space_info["repo_name_md"] = full_space_name_md
130
  logger.debug(f"Parsed space header: {space_info['owner_md']}/{space_info['repo_name_md']}")
131
  continue
 
132
  if line_content_stripped.startswith("## File Structure"):
 
133
  structure_block_start = i + 1
134
  while structure_block_start < len(lines) and not lines[structure_block_start].strip().startswith("```"):
135
  structure_block_start += 1
136
  if structure_block_start < len(lines) and lines[structure_block_start].strip().startswith("```"):
 
137
  structure_block_end = structure_block_start + 1
138
  while structure_block_end < len(lines) and not lines[structure_block_end].strip().startswith("```"):
139
  structure_block_end += 1
140
  if structure_block_end < len(lines) and lines[structure_block_end].strip().startswith("```"):
 
141
  logger.debug(f"Skipping File Structure block from line {i+1} to {structure_block_end+1}")
142
+ i = structure_block_end
143
  continue
 
144
  continue
145
 
 
146
  if in_file_definition:
 
147
  if line_content_stripped.startswith("```"):
 
148
  in_code_block = not in_code_block
 
149
  logger.debug(f"Toggled code block to {in_code_block} at line {line_num}")
150
+ continue
151
 
 
152
  if in_code_block:
153
  current_file_content_lines.append(line_content_orig)
 
154
  elif line_content_stripped.startswith("[Binary file") or line_content_stripped.startswith("[Error loading content:") or line_content_stripped.startswith("[Binary or Skipped file]"):
 
155
  current_file_content_lines.append(line_content_orig)
156
  logger.debug(f"Parsed binary/error marker for {current_file_path} at line {line_num}")
 
 
157
  else:
 
 
158
  pass
159
 
 
 
160
  if current_file_path is not None and in_file_definition:
161
  content_to_save = "\n".join(current_file_content_lines).strip()
162
  space_info["files"].append({"path": current_file_path, "content": content_to_save})
163
 
164
+ space_info["files"] = [f for f in space_info["files"] if f.get("path")]
 
 
 
 
 
165
  space_info["owner_md"] = space_info["owner_md"].strip()
166
  space_info["repo_name_md"] = space_info["repo_name_md"].strip()
167
 
168
  if file_parsing_errors:
169
  logger.warning(f"Markdown parsing encountered errors: {file_parsing_errors}")
 
 
170
 
171
  logger.info(f"Parsed markdown. Found {len(space_info['files'])} files.")
172
  return space_info
173
 
 
 
174
  def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui):
175
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
176
  sdk = None
177
  files = []
178
  error = None
179
+ repo_id = None
180
 
181
  logger.info(f"Attempting to get repo info for {repo_id_for_error_logging}")
182
 
 
184
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
185
  if token_err: return None, [], token_err
186
 
187
+ repo_id, err_repo_id = _determine_repo_id(resolved_api_token, owner_ui, space_name_ui)
188
  if err_repo_id: return None, [], err_repo_id
189
+ repo_id_for_error_logging = repo_id
190
 
191
  api = HfApi(token=resolved_api_token)
192
+ repo_info_obj = api.repo_info(repo_id=repo_id, repo_type="space", timeout=20)
 
193
  sdk = repo_info_obj.sdk
194
  files = [sibling.rfilename for sibling in repo_info_obj.siblings if sibling.rfilename]
195
 
196
  if not files and repo_info_obj.siblings:
197
  logger.warning(f"Repo {repo_id} has siblings but no rfilenames extracted. Total siblings: {len(repo_info_obj.siblings)}")
 
 
198
 
199
  logger.info(f"Successfully got repo info for {repo_id}. SDK: {sdk}, Files found: {len(files)}")
200
 
201
 
202
+ except HfHubHTTPError as e_http:
203
  logger.error(f"HTTP error getting repo info for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
204
  error_message = str(e_http)
205
  status_code = e_http.response.status_code if e_http.response is not None else None
 
211
  else:
212
  error = f"HTTP Error {status_code or 'unknown'} for '{repo_id_for_error_logging or 'unknown repo'}': {error_message}"
213
 
214
+ except Exception as e:
 
215
  logger.warning(f"Could not get full repo_info for {repo_id_for_error_logging or 'unknown repo'}, attempting list_repo_files fallback: {e}")
216
+ error = f"Error retrieving Space info for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e)}. Attempting file list fallback."
217
 
218
  try:
 
219
  resolved_api_token_fb, token_err_fb = _get_api_token(ui_api_token_from_textbox)
220
+ if token_err_fb: return None, [], f"{error}\nAPI Token Error during fallback: {token_err_fb}"
221
+ repo_id_fb, err_repo_id_fb = _determine_repo_id(resolved_api_token_fb, owner_ui, space_name_ui)
222
+ if err_repo_id_fb: return None, [], f"{error}\nRepo ID Error during fallback: {err_repo_id_fb}"
223
 
224
+ files = list_repo_files(repo_id=repo_id_fb, token=resolved_api_token_fb, repo_type="space", timeout=20)
 
 
225
  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."
226
  logger.info(f"Fallback list_repo_files successful for {repo_id_fb}. Files found: {len(files)}")
227
 
 
233
  error = f"Space '{repo_id_for_error_logging or 'unknown repo'}' not found during fallback (404)."
234
  else:
235
  error = f"HTTP Error {status_code_fb or 'unknown'} for '{repo_id_for_error_logging or 'unknown repo'}' during fallback: {error_message_fb}"
236
+ files = []
237
 
238
  except Exception as e2:
239
  logger.exception(f"Error listing files for {repo_id_for_error_logging or 'unknown repo'} during fallback: {e2}")
240
  error = f"{error}\nError listing files during fallback for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e2)}"
241
+ files = []
 
242
 
 
 
243
  if not files and not error and (repo_id_for_error_logging is not None):
244
  error = f"No files found in Space `{repo_id_for_error_logging or 'unknown repo'}`."
245
 
246
  return sdk, files, error
247
 
 
 
248
  def list_space_files_for_browsing(ui_api_token_from_textbox, space_name_ui, owner_ui):
249
  files, err = get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui)[1:]
250
  return files, err
251
 
 
 
252
  def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo):
253
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
254
  repo_id = None
 
256
  try:
257
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
258
  if token_err: return None, token_err
259
+ repo_id, err_repo_id = _determine_repo_id(resolved_api_token, owner_ui, space_name_ui)
260
  if err_repo_id: return None, err_repo_id
261
+ repo_id_for_error_logging = repo_id
262
 
263
  if not file_path_in_repo: return None, "Error: File path cannot be empty."
 
264
  file_path_in_repo = file_path_in_repo.replace("\\", "/")
265
 
 
266
  downloaded_file_path = hf_hub_download(
267
  repo_id=repo_id,
268
  filename=file_path_in_repo,
269
  repo_type="space",
270
  token=resolved_api_token,
271
+ local_dir_use_symlinks=False,
272
+ cache_dir=None,
273
+ timeout=20
274
  )
275
  content = Path(downloaded_file_path).read_text(encoding="utf-8")
276
  logger.info(f"Successfully downloaded and read content for '{file_path_in_repo}'.")
277
  return content, None
278
  except FileNotFoundError:
279
  logger.error(f"FileNotFoundError for '{file_path_in_repo}' in {repo_id_for_error_logging or 'unknown'}")
280
+ return None, f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging or 'unknown repo'}' (404)."
281
  except UnicodeDecodeError:
 
282
  logger.warning(f"UnicodeDecodeError for '{file_path_in_repo}'. Likely binary.")
283
  return None, f"Error: File '{file_path_in_repo}' is not valid UTF-8 text. Cannot display."
284
  except HfHubHTTPError as e_http:
 
294
  logger.exception(f"Error fetching file content for {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}:")
295
  return None, f"Error fetching file content: {str(e)}"
296
 
 
 
 
297
  def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, changeset):
298
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
299
  repo_id = None
300
  status_messages = []
 
301
 
302
  logger.info(f"Attempting to apply {len(changeset)} staged changes to {repo_id_for_error_logging}")
303
 
304
  try:
305
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
306
+ if token_err: return f"API Token Error: {token_err}"
307
 
308
+ repo_id, err_repo_id = _determine_repo_id(resolved_api_token, owner_ui, space_name_ui)
309
+ if err_repo_id: return f"Repo ID Error: {err_repo_id}"
310
  repo_id_for_error_logging = repo_id
311
 
312
  api = HfApi(token=resolved_api_token)
313
 
 
314
  create_space_op = next((c for c in changeset if c['type'] == 'CREATE_SPACE'), None)
315
  if create_space_op:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  try:
317
  api.create_repo(repo_id=repo_id, repo_type="space", space_sdk=create_space_op.get('sdk', 'gradio'), private=create_space_op.get('private', False), exist_ok=True)
318
+ status_messages.append(f"CREATE_SPACE: Successfully created or ensured space [{repo_id}](https://huggingface.co/spaces/{repo_id}) exists.")
319
  logger.info(f"Successfully created or ensured space {repo_id} exists.")
320
+ except HfHubHTTPError as e_http:
321
+ status_messages.append(f"CREATE_SPACE HTTP Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check logs.")
322
+ logger.error(f"HTTP error creating space {repo_id}: {e_http}")
323
  except Exception as e:
324
  status_messages.append(f"CREATE_SPACE Error: {e}")
325
  logger.error(f"Error creating space {repo_id}: {e}")
326
+
 
 
 
 
 
 
 
327
  temp_dir = None
328
+ paths_to_upload = {}
329
+ delete_operations = []
330
 
331
  try:
332
  temp_dir = tempfile.TemporaryDirectory()
333
  repo_staging_path = Path(temp_dir.name) / "repo_staging_content"
334
  repo_staging_path.mkdir(exist_ok=True)
335
 
 
336
  gitattributes_path_local = repo_staging_path / ".gitattributes"
337
+ try:
338
+ with open(gitattributes_path_local, "w", encoding="utf-8") as f:
339
+ f.write("* text=auto eol=lf\n")
340
+ paths_to_upload[str(gitattributes_path_local)] = ".gitattributes"
341
+ except Exception as e:
342
+ status_messages.append(f"Warning: Could not stage .gitattributes file: {e}")
343
+ logger.warning(f"Could not stage .gitattributes: {e}")
344
 
345
 
346
  for change in changeset:
 
351
  continue
352
 
353
  content_to_write = change.get('content', '')
 
354
  if content_to_write.startswith("[Binary file") or content_to_write.startswith("[Error loading content:") or content_to_write.startswith("[Binary or Skipped file]"):
355
  status_messages.append(f"Skipping {change['type']} for '{file_path_in_repo}': Content is a binary/error placeholder.")
356
  logger.warning(f"Skipping {change['type']} operation for '{file_path_in_repo}': Content is binary/error placeholder.")
357
  continue
358
 
359
  file_path_local = repo_staging_path / file_path_in_repo
360
+ file_path_local.parent.mkdir(parents=True, exist_ok=True)
361
 
362
  try:
363
  with open(file_path_local, "w", encoding="utf-8") as f:
364
  f.write(content_to_write)
365
  paths_to_upload[str(file_path_local)] = file_path_in_repo
366
+ logger.debug(f"Staged file for {change['type']}: {file_path_in_repo}")
367
  except Exception as file_write_error:
368
  status_messages.append(f"Error staging file {file_path_in_repo} for {change['type']}: {file_write_error}")
369
  logger.error(f"Error writing file {file_path_in_repo} during staging for {change['type']}: {file_write_error}")
 
374
  if not file_path_in_repo:
375
  status_messages.append(f"Skipping DELETE_FILE operation: empty path.")
376
  continue
377
+ delete_operations.append(CommitOperationDelete(path_in_repo=file_path_in_repo))
378
+ logger.debug(f"Added DELETE_FILE operation for: {file_path_in_repo}")
379
+
380
+
381
+ if delete_operations:
382
+ try:
383
+ commit_message_delete = f"AI Space Builder: Deleted {len(delete_operations)} files."
384
+ logger.info(f"Performing delete commit for {repo_id_for_error_logging}: {commit_message_delete}")
385
+ api.create_commit(
386
+ repo_id=repo_id,
387
+ repo_type="space",
388
+ operations=delete_operations,
389
+ commit_message=commit_message_delete
390
+ )
391
+ status_messages.append(f"File Deletions: Successfully committed {len(delete_operations)} deletions.")
392
+ logger.info("Delete commit successful.")
393
+ except HfHubHTTPError as e_http:
394
+ status_messages.append(f"File Deletion HTTP Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check logs.")
395
+ logger.error(f"HTTP error during delete commit for {repo_id}: {e_http}")
396
+ except Exception as e_delete_commit:
397
+ status_messages.append(f"File Deletion Error: {str(e_delete_commit)}. Check logs.")
398
+ logger.exception(f"Error during delete commit for {repo_id}:")
399
+
400
+
401
+ if paths_to_upload:
402
+ try:
403
+ commit_message_upload = f"AI Space Builder: Updated Space content for {repo_id}"
404
+ logger.info(f"Uploading staged files from {str(repo_staging_path)} to {repo_id}...")
405
+ upload_folder(
406
+ repo_id=repo_id,
407
+ folder_path=str(repo_staging_path),
408
+ path_in_repo=".",
409
+ token=resolved_api_token,
410
+ repo_type="space",
411
+ commit_message=commit_message_upload,
412
+ allow_patterns=["*"],
413
+ )
414
+ status_messages.append(f"File Uploads/Updates: Successfully uploaded/updated {len(paths_to_upload)} files.")
415
+ logger.info("Upload/Update commit successful.")
416
+ except HfHubHTTPError as e_http:
417
+ status_messages.append(f"File Upload/Update HTTP Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check logs.")
418
+ logger.error(f"HTTP error during upload_folder for {repo_id}: {e_http}")
419
+ except Exception as e_upload:
420
+ status_messages.append(f"File Upload/Update Error: {str(e_upload)}. Check logs.")
421
+ logger.exception(f"Error during upload_folder for {repo_id}:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
  else:
424
  status_messages.append("No file changes (create/update/delete) to commit.")
 
426
 
427
 
428
  finally:
 
429
  if temp_dir:
430
  try:
431
  temp_dir.cleanup()
 
433
  except Exception as e:
434
  logger.error(f"Error cleaning up temp dir: {e}")
435
 
 
 
436
  for change in changeset:
437
  if change['type'] == 'SET_PRIVACY':
438
  try:
439
+ target_repo_id = change.get('repo_id', repo_id)
440
  if not target_repo_id:
441
  status_messages.append("SET_PRIVACY Error: Target repo_id not specified.")
442
  continue
 
444
  status_messages.append(f"SET_PRIVACY: Successfully set `{target_repo_id}` to `private={change['private']}`.")
445
  logger.info(f"Successfully set privacy for {target_repo_id} to {change['private']}.")
446
  except HfHubHTTPError as e_http:
447
+ status_messages.append(f"SET_PRIVACY HTTP Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check token/permissions.")
448
  logger.error(f"HTTP error setting privacy for {target_repo_id}: {e_http}")
449
  except Exception as e:
450
  status_messages.append(f"SET_PRIVACY Error: {str(e)}. Check logs.")
451
  logger.exception(f"Error setting privacy for {target_repo_id}:")
452
 
453
  elif change['type'] == 'DELETE_SPACE':
454
+ delete_owner = change.get('owner') or owner_ui
455
+ delete_space = change.get('space_name') or space_name_ui
 
456
  delete_repo_id = f"{delete_owner}/{delete_space}" if delete_owner and delete_space else repo_id
457
 
458
  if not delete_repo_id:
459
  status_messages.append("DELETE_SPACE Error: Target repo_id not specified.")
460
  continue
461
 
 
 
 
 
 
 
462
  if delete_repo_id != repo_id:
463
  status_messages.append(f"DELETE_SPACE Error: AI requested deletion of '{delete_repo_id}', but this action is only permitted for the currently loaded space '{repo_id}'. Action blocked.")
464
  logger.warning(f"Blocked DELETE_SPACE action: requested '{delete_repo_id}', current '{repo_id}'.")
465
+ continue
466
 
467
  logger.warning(f"Attempting DESTRUCTIVE DELETE_SPACE action for {delete_repo_id}")
468
  try:
469
  api.delete_repo(repo_id=delete_repo_id, repo_type='space')
470
  status_messages.append(f"DELETE_SPACE: Successfully deleted space `{delete_repo_id}`.")
471
+ logger.warning(f"Successfully deleted space {delete_repo_id}.")
472
  except HfHubHTTPError as e_http:
473
+ status_messages.append(f"DELETE_SPACE HTTP Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}. Check token/permissions.")
474
  logger.error(f"HTTP error deleting space {delete_repo_id}: {e_http}")
475
  except Exception as e:
476
  status_messages.append(f"DELETE_SPACE Error: {str(e)}. Check logs.")
477
  logger.exception(f"Error deleting space {delete_repo_id}:")
478
 
 
479
  except HfHubHTTPError as e_http:
480
  logger.error(f"Top-level HTTP error during apply_staged_changes for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
481
  status_messages.append(f"API HTTP Error ({e_http.response.status_code if e_http.response else 'N/A'}): {e_http.response.text if e_http.response else str(e_http)}")
482
  except Exception as e:
483
  logger.exception(f"Top-level error during apply_staged_changes for {repo_id_for_error_logging or 'unknown repo'}:")
484
+ status_messages.append(f"An unexpected error occurred during apply staged changes: {str(e)}")
485
 
 
486
  final_status = " | ".join(status_messages) if status_messages else "No operations were applied."
487
  logger.info(f"Finished applying staged changes. Final status: {final_status}")
488
  return final_status
489
 
 
 
 
490
  def delete_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, commit_message_ui=None):
491
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
492
  repo_id = None
 
494
  try:
495
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
496
  if token_err: return f"API Token Error: {token_err}"
497
+ repo_id, err_repo_id = _determine_repo_id(resolved_api_token, owner_ui, space_name_ui)
498
  if err_repo_id: return f"Repo ID Error: {err_repo_id}"
499
+ repo_id_for_error_logging = repo_id
500
 
501
  if not file_path_in_repo: return "Error: File path cannot be empty for deletion."
502
+ file_path_in_repo = file_path_in_repo.lstrip('/').replace(os.sep, '/')
 
 
 
503
 
504
  effective_commit_message = commit_message_ui or f"Deleted file: {file_path_in_repo} via AI Space Editor UI"
505
 
 
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
+ timeout=20
513
  )
514
  logger.info(f"Successfully deleted file: {file_path_in_repo}")
515
  return f"Successfully deleted file: `{file_path_in_repo}`"
516
 
517
  except FileNotFoundError:
518
  logger.error(f"FileNotFoundError during manual delete for '{file_path_in_repo}' in {repo_id_for_error_logging or 'unknown'}")
519
+ return f"Delete Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging or 'unknown repo'}' (404)."
520
+ except HfHubHTTPError as e_http:
521
  logger.error(f"HTTP error deleting file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
522
  error_message = str(e_http)
523
  status_code = e_http.response.status_code if e_http.response is not None else None
524
 
525
  if status_code == 404:
526
+ return f"Delete Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging or 'unknown repo'}' for deletion (404)."
527
  if status_code in (401, 403):
528
+ return f"Delete Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
529
+ return f"Delete HTTP Error {status_code or 'unknown'} deleting file '{file_path_in_repo}': {error_message}"
530
  except Exception as e:
531
  logger.exception(f"Error deleting file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}:")
532
+ return f"Delete Error deleting file '{file_path_in_repo}': {str(e)}"
533
 
 
 
534
  def update_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, file_content, commit_message_ui):
535
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
536
  repo_id = None
537
  logger.info(f"Attempting manual file update for '{file_path_in_repo}' in {repo_id_for_error_logging}")
538
  try:
539
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
540
+ if token_err: return f"API Token Error: {token_err}"
541
+ repo_id, err_repo_id = _determine_repo_id(resolved_api_token, owner_ui, space_name_ui)
542
+ if err_repo_id: return f"Repo ID Error: {err_repo_id}"
543
+ repo_id_for_error_logging = repo_id
544
 
545
+ if not file_path_in_repo: return "Update Error: File Path to update cannot be empty."
546
+ file_path_in_repo = file_path_in_repo.lstrip('/').replace(os.sep, '/')
547
  commit_msg = commit_message_ui or f"Update {file_path_in_repo} via AI Space Editor UI"
548
 
549
  api = HfApi(token=resolved_api_token)
550
 
 
551
  tmp_file_path = None
552
  try:
553
  with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as tmp_file_obj:
554
  tmp_file_obj.write(file_content)
555
  tmp_file_path = tmp_file_obj.name
556
 
 
557
  api.upload_file(
558
  path_or_fileobj=tmp_file_path,
559
  path_in_repo=file_path_in_repo,
560
  repo_id=repo_id,
561
  repo_type="space",
562
  commit_message=commit_msg,
563
+ timeout=20
564
  )
565
  logger.info(f"Successfully updated file: {file_path_in_repo}")
566
  return f"Successfully updated `{file_path_in_repo}`"
567
  finally:
 
568
  if tmp_file_path and os.path.exists(tmp_file_path):
569
  os.remove(tmp_file_path)
570
 
571
  except FileNotFoundError:
572
  logger.error(f"FileNotFoundError during manual update for '{file_path_in_repo}' in {repo_id_for_error_logging or 'unknown'}")
573
+ return f"Update Error: Local temporary file not found during upload for '{file_path_in_repo}'."
574
  except UnicodeDecodeError:
 
575
  logger.warning(f"UnicodeDecodeError for '{file_path_in_repo}' during manual update.")
576
+ return f"Update Error: Content for '{file_path_in_repo}' is not valid UTF-8 text. Cannot edit this way."
577
  except HfHubHTTPError as e_http:
578
  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}")
579
  error_message = str(e_http)
580
  status_code = e_http.response.status_code if e_http.response is not None else None
581
  if status_code == 404:
582
+ return f"Update Error: Space '{repo_id_for_error_logging or 'unknown repo'}' or file '{file_path_in_repo}' not found (404)."
583
  if status_code in (401, 403):
584
+ return f"Update Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
585
+ return f"Update HTTP Error {status_code or 'unknown'} updating file '{file_path_in_repo}': {error_message}"
586
  except Exception as e:
587
  logger.exception(f"Error in update_space_file for {repo_id_for_error_logging or 'unknown repo'}, file {file_path_in_repo}:")
588
+ return f"Update Error updating file for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e)}"
589
 
 
 
590
  def get_space_runtime_status(ui_api_token_from_textbox, space_name_ui, owner_ui):
591
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
592
  repo_id = None
 
594
  try:
595
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
596
  if token_err: return None, f"API Token Error: {token_err}"
597
+ repo_id, err_repo_id = _determine_repo_id(resolved_api_token, owner_ui, space_name_ui)
598
  if err_repo_id: return None, f"Repo ID Error: {err_repo_id}"
599
+ repo_id_for_error_logging = repo_id
600
 
601
  api = HfApi(token=resolved_api_token)
602
 
 
 
603
  runtime_info = api.get_space_runtime(repo_id=repo_id, timeout=20)
604
  logger.info(f"Received runtime info for {repo_id}. Stage: {runtime_info.stage}")
605
 
 
 
606
  status_details = {
607
  "stage": runtime_info.stage,
608
  "hardware": runtime_info.hardware,
609
+ "requested_hardware": runtime_info.requested_hardware if hasattr(runtime_info, 'requested_hardware') else None,
610
+ "error_message": None,
611
+ "status": runtime_info.status if hasattr(runtime_info, 'status') else None,
612
  "full_log_link": f"https://huggingface.co/spaces/{repo_id}/logs" if repo_id else "#"
 
613
  }
614
 
 
615
  if runtime_info.stage == "ERRORED":
616
  error_content = None
 
617
  if hasattr(runtime_info, 'error') and runtime_info.error: error_content = str(runtime_info.error)
 
618
  if 'build' in runtime_info.raw and isinstance(runtime_info.raw['build'], dict) and runtime_info.raw['build'].get('status') == 'error':
619
  error_content = f"Build Error: {runtime_info.raw['build'].get('message', error_content or 'Unknown build error')}"
620
  elif 'run' in runtime_info.raw and isinstance(runtime_info.raw['run'], dict) and runtime_info.raw['run'].get('status') == 'error':
621
  error_content = f"Runtime Error: {runtime_info.raw['run'].get('message', error_content or 'Unknown runtime error')}"
622
+ 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('!')):
623
+ error_content = runtime_info.raw['message']
624
 
625
  status_details["error_message"] = error_content if error_content else "Space is in an errored state. Check logs for details."
626
 
627
  logger.info(f"Runtime status details for {repo_id}: {status_details}")
628
  return status_details, None
629
 
630
+ except HfHubHTTPError as e_http:
631
  logger.error(f"HTTP error fetching runtime status for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
632
  error_message = str(e_http)
633
  status_code = e_http.response.status_code if e_http.response is not None else None
634
 
635
  if status_code == 404:
636
+ return None, f"Status Error: Space '{repo_id_for_error_logging or 'unknown repo'}' not found or has no active runtime status (404)."
 
637
  if status_code in (401, 403):
638
+ return None, f"Status Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
639
+ return None, f"Status HTTP Error {status_code or 'unknown'} fetching runtime status for '{repo_id_for_error_logging or 'unknown repo'}': {error_message}"
640
 
641
  except Exception as e:
642
  logger.exception(f"Error fetching runtime status for {repo_id_for_error_logging or 'unknown repo'}:")
643
+ return None, f"Status Error fetching runtime status: {str(e)}"
644
 
 
645
  def build_logic_set_space_privacy(hf_api_key, repo_id, private: bool):
 
646
  logger.info(f"Attempting to set privacy for '{repo_id}' to {private}.")
647
  try:
648
  token, err = _get_api_token(hf_api_key)
 
661
  logger.exception(f"Error setting privacy for {repo_id}:")
662
  return f"Error setting privacy for `{repo_id}`: {e}"
663
 
 
664
  def build_logic_delete_space(hf_api_key, owner, space_name):
 
665
  repo_id = f"{owner}/{space_name}"
666
  logger.warning(f"Attempting DESTRUCTIVE DELETE_SPACE action for '{repo_id}'.")
667
  try:
 
679
  return f"HTTP Error ({status_code}) deleting space `{repo_id}`: {e_http.response.text if e_http.response else str(e_http)}"
680
  except Exception as e:
681
  logger.exception(f"Error deleting space {repo_id}:")
682
+ return f"Error deleting space `{repo_id}`: {e}"