Update build_logic.py
Browse files- build_logic.py +453 -217
build_logic.py
CHANGED
@@ -14,7 +14,7 @@ from huggingface_hub import (
|
|
14 |
delete_file as hf_delete_file,
|
15 |
HfApi
|
16 |
)
|
17 |
-
from huggingface_hub.hf_api import CommitOperationDelete
|
18 |
# Import the general HTTP error from huggingface_hub.utils
|
19 |
from huggingface_hub.utils import HfHubHTTPError # For catching specific HF HTTP errors
|
20 |
|
@@ -28,8 +28,13 @@ logger = logging.getLogger(__name__)
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
33 |
return None, "Error: Hugging Face API token not provided in UI or HF_TOKEN env var."
|
34 |
|
35 |
# --- Helper Function to Determine Repo ID ---
|
@@ -41,6 +46,7 @@ def _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui):
|
|
41 |
error_message = None
|
42 |
|
43 |
if not final_owner:
|
|
|
44 |
resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
|
45 |
if token_err: return None, token_err
|
46 |
if not resolved_api_token: return None, "Error: API token required for auto owner determination if Owner field is empty."
|
@@ -48,23 +54,32 @@ def _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui):
|
|
48 |
user_info = whoami(token=resolved_api_token)
|
49 |
if user_info and 'name' in user_info:
|
50 |
final_owner = user_info['name']
|
|
|
51 |
else:
|
52 |
error_message = "Error: Could not retrieve username from token. Check token permissions or specify Owner."
|
|
|
53 |
except Exception as e:
|
54 |
error_message = f"Error retrieving username from token: {str(e)}. Specify Owner or check token."
|
|
|
55 |
if error_message: return None, error_message
|
56 |
|
57 |
if not final_owner: return None, "Error: Owner could not be determined. Please specify it in the Owner field."
|
58 |
-
|
|
|
|
|
59 |
|
60 |
|
61 |
# --- Corrected Markdown Parsing ---
|
|
|
|
|
|
|
62 |
def parse_markdown(markdown_input):
|
63 |
space_info = {"repo_name_md": "", "owner_md": "", "files": []}
|
64 |
current_file_path = None
|
65 |
current_file_content_lines = []
|
66 |
in_file_definition = False
|
67 |
in_code_block = False
|
|
|
68 |
|
69 |
lines = markdown_input.strip().split("\n")
|
70 |
|
@@ -85,27 +100,39 @@ def parse_markdown(markdown_input):
|
|
85 |
lines = cleaned_lines
|
86 |
|
87 |
|
88 |
-
for line_content_orig in lines:
|
89 |
line_content_stripped = line_content_orig.strip()
|
|
|
90 |
|
91 |
-
|
|
|
|
|
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 |
-
|
|
|
|
|
95 |
|
96 |
-
|
|
|
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 |
-
|
|
|
107 |
|
108 |
-
# If
|
109 |
if not in_file_definition:
|
110 |
if line_content_stripped.startswith("# Space:"):
|
111 |
full_space_name_md = line_content_stripped.replace("# Space:", "").strip()
|
@@ -117,49 +144,73 @@ def parse_markdown(markdown_input):
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
#
|
129 |
-
|
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 |
-
|
|
|
|
|
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 |
|
@@ -171,22 +222,29 @@ def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui
|
|
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)
|
176 |
-
if token_err: return None,
|
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,
|
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=
|
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}")
|
@@ -208,14 +266,15 @@ def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui
|
|
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,
|
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,
|
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=
|
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}")
|
@@ -234,15 +293,16 @@ def get_space_repository_info(ui_api_token_from_textbox, space_name_ui, owner_ui
|
|
234 |
|
235 |
|
236 |
# Final check: if files are still empty and there's no specific error, provide a generic "no files" message
|
237 |
-
if
|
238 |
-
|
|
|
239 |
|
240 |
return sdk, files, error
|
241 |
|
242 |
|
243 |
# --- Function to list files ---
|
244 |
def list_space_files_for_browsing(ui_api_token_from_textbox, space_name_ui, owner_ui):
|
245 |
-
|
246 |
return files, err
|
247 |
|
248 |
|
@@ -250,12 +310,14 @@ def list_space_files_for_browsing(ui_api_token_from_textbox, space_name_ui, owne
|
|
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
|
256 |
repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
|
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("\\", "/")
|
@@ -267,14 +329,18 @@ def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, f
|
|
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 |
-
|
|
|
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}")
|
@@ -289,151 +355,314 @@ def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, f
|
|
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 |
-
|
|
|
|
|
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 |
-
|
300 |
-
|
301 |
-
|
|
|
302 |
|
303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
304 |
|
305 |
-
|
306 |
-
|
|
|
307 |
repo_staging_path.mkdir(exist_ok=True)
|
308 |
|
309 |
-
# Always
|
310 |
-
|
311 |
-
with open(
|
312 |
f.write("* text=auto eol=lf\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
313 |
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
logger.info(f"Markdown contained no standard files. Staging only .gitattributes for {repo_id}.")
|
318 |
|
319 |
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
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 |
-
|
345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
346 |
|
347 |
-
|
|
|
348 |
|
349 |
-
|
350 |
-
|
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 |
-
|
419 |
|
420 |
-
|
421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
422 |
error_message = str(e_http)
|
423 |
status_code = e_http.response.status_code if e_http.response is not None else None
|
424 |
-
|
425 |
-
|
|
|
426 |
if status_code in (401, 403):
|
427 |
-
return f"Error
|
428 |
-
return f"HTTP Error {status_code or 'unknown'}
|
429 |
except Exception as e:
|
430 |
-
logger.exception(f"Error
|
431 |
-
return f"Error
|
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
|
@@ -443,35 +672,40 @@ def update_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_p
|
|
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 |
-
|
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 |
-
|
|
|
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 UnicodeDecodeError:
|
473 |
# If read_text fails, it's likely binary or non-utf8 text
|
474 |
-
|
|
|
475 |
except HfHubHTTPError as e_http:
|
476 |
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}")
|
477 |
error_message = str(e_http)
|
@@ -486,53 +720,11 @@ def update_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_p
|
|
486 |
return f"Error updating file for `{repo_id_for_error_logging or 'unknown repo'}`: {str(e)}"
|
487 |
|
488 |
|
489 |
-
# --- Delete Single File ---
|
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
|
493 |
-
try:
|
494 |
-
resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
|
495 |
-
if token_err: return f"API Token Error: {token_err}"
|
496 |
-
repo_id, err_repo_id = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
|
497 |
-
if err_repo_id: return f"Repo ID Error: {err_repo_id}"
|
498 |
-
repo_id_for_error_logging = repo_id # Update logging name
|
499 |
-
|
500 |
-
if not file_path_in_repo: return "Error: File path cannot be empty for deletion."
|
501 |
-
file_path_in_repo = file_path_in_repo.lstrip('/').replace(os.sep, '/') # Clean path for Hub
|
502 |
-
|
503 |
-
# Prevent deleting essential files like .gitattributes or README.md unless explicitly handled?
|
504 |
-
# For now, allow deleting anything selected in the dropdown.
|
505 |
-
|
506 |
-
effective_commit_message = commit_message_ui or f"Deleted file: {file_path_in_repo} via AI Space Editor"
|
507 |
-
|
508 |
-
# Use hf_delete_file directly
|
509 |
-
hf_delete_file(
|
510 |
-
path_in_repo=file_path_in_repo,
|
511 |
-
repo_id=repo_id,
|
512 |
-
repo_type="space",
|
513 |
-
token=resolved_api_token,
|
514 |
-
commit_message=effective_commit_message
|
515 |
-
)
|
516 |
-
return f"Successfully deleted file: {file_path_in_repo}"
|
517 |
-
|
518 |
-
except HfHubHTTPError as e_http: # Catch specific HF HTTP errors
|
519 |
-
logger.error(f"HTTP error deleting file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
|
520 |
-
error_message = str(e_http)
|
521 |
-
status_code = e_http.response.status_code if e_http.response is not None else None
|
522 |
-
|
523 |
-
if status_code == 404:
|
524 |
-
return f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging or 'unknown repo'}' for deletion (404)."
|
525 |
-
if status_code in (401, 403):
|
526 |
-
return f"Error: Access denied or authentication required for '{repo_id_for_error_logging or 'unknown repo'}' ({status_code}). Check token permissions."
|
527 |
-
return f"HTTP Error {status_code or 'unknown'} deleting file '{file_path_in_repo}': {error_message}"
|
528 |
-
except Exception as e:
|
529 |
-
logger.exception(f"Error deleting file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}:")
|
530 |
-
return f"Error deleting file '{file_path_in_repo}': {str(e)}"
|
531 |
-
|
532 |
# --- Get Space Runtime Status ---
|
533 |
def get_space_runtime_status(ui_api_token_from_textbox, space_name_ui, owner_ui):
|
534 |
repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
|
535 |
repo_id = None
|
|
|
536 |
try:
|
537 |
resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
|
538 |
if token_err: return None, f"API Token Error: {token_err}"
|
@@ -541,19 +733,22 @@ def get_space_runtime_status(ui_api_token_from_textbox, space_name_ui, owner_ui)
|
|
541 |
repo_id_for_error_logging = repo_id # Update logging name
|
542 |
|
543 |
api = HfApi(token=resolved_api_token)
|
544 |
-
logger.info(f"Fetching runtime status for Space: {repo_id}")
|
545 |
|
546 |
# Use get_space_runtime which provides details like stage, hardware, etc.
|
547 |
-
|
|
|
|
|
|
|
548 |
|
549 |
# Structure the details for display
|
550 |
status_details = {
|
551 |
"stage": runtime_info.stage,
|
552 |
"hardware": runtime_info.hardware,
|
553 |
"requested_hardware": runtime_info.requested_hardware if hasattr(runtime_info, 'requested_hardware') else None, # requested_hardware might not always be present
|
554 |
-
"error_message": None,
|
555 |
-
"
|
556 |
-
"
|
|
|
557 |
}
|
558 |
|
559 |
# Check for specific error states or messages
|
@@ -561,19 +756,17 @@ def get_space_runtime_status(ui_api_token_from_textbox, space_name_ui, owner_ui)
|
|
561 |
error_content = None
|
562 |
# Look for error details in various places within the raw data or the error attribute
|
563 |
if hasattr(runtime_info, 'error') and runtime_info.error: error_content = str(runtime_info.error)
|
564 |
-
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
|
565 |
-
error_content = runtime_info.raw['message']
|
566 |
-
elif 'error' in runtime_info.raw: error_content = str(runtime_info.raw['error'])
|
567 |
-
|
568 |
# Check build/run specific error messages in raw data
|
569 |
if 'build' in runtime_info.raw and isinstance(runtime_info.raw['build'], dict) and runtime_info.raw['build'].get('status') == 'error':
|
570 |
error_content = f"Build Error: {runtime_info.raw['build'].get('message', error_content or 'Unknown build error')}"
|
571 |
elif 'run' in runtime_info.raw and isinstance(runtime_info.raw['run'], dict) and runtime_info.raw['run'].get('status') == 'error':
|
572 |
error_content = f"Runtime Error: {runtime_info.raw['run'].get('message', error_content or 'Unknown runtime error')}"
|
|
|
|
|
573 |
|
574 |
status_details["error_message"] = error_content if error_content else "Space is in an errored state. Check logs for details."
|
575 |
|
576 |
-
logger.info(f"Runtime status for {repo_id}: {status_details
|
577 |
return status_details, None
|
578 |
|
579 |
except HfHubHTTPError as e_http: # Catch specific HF HTTP errors
|
@@ -590,4 +783,47 @@ def get_space_runtime_status(ui_api_token_from_textbox, space_name_ui, owner_ui)
|
|
590 |
|
591 |
except Exception as e:
|
592 |
logger.exception(f"Error fetching runtime status for {repo_id_for_error_logging or 'unknown repo'}:")
|
593 |
-
return None, f"Error fetching runtime status: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
delete_file as hf_delete_file,
|
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 |
|
|
|
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 ---
|
|
|
46 |
error_message = None
|
47 |
|
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."
|
|
|
54 |
user_info = whoami(token=resolved_api_token)
|
55 |
if user_info and 'name' in user_info:
|
56 |
final_owner = user_info['name']
|
57 |
+
logger.info(f"Auto-detected owner: {final_owner}")
|
58 |
else:
|
59 |
error_message = "Error: Could not retrieve username from token. Check token permissions or specify Owner."
|
60 |
+
logger.error(error_message)
|
61 |
except Exception as e:
|
62 |
error_message = f"Error retrieving username from token: {str(e)}. Specify Owner or check token."
|
63 |
+
logger.exception("Error retrieving username from token:")
|
64 |
if error_message: return None, error_message
|
65 |
|
66 |
if not final_owner: return None, "Error: Owner could not be determined. Please specify it in the Owner field."
|
67 |
+
repo_id = f"{final_owner}/{space_name_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 |
|
|
|
100 |
lines = cleaned_lines
|
101 |
|
102 |
|
103 |
+
for i, line_content_orig in enumerate(lines):
|
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()
|
|
|
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 |
|
|
|
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 |
+
|
227 |
try:
|
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}")
|
|
|
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 |
|
279 |
except HfHubHTTPError as e2_http:
|
280 |
logger.error(f"HTTP error during fallback list_repo_files for {repo_id_for_error_logging or 'unknown repo'}: {e2_http}")
|
|
|
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 |
|
|
|
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
|
313 |
+
logger.info(f"Attempting to get content for file '{file_path_in_repo}' from {repo_id_for_error_logging}")
|
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("\\", "/")
|
|
|
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:
|
346 |
logger.error(f"HTTP error fetching file {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
|
|
|
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:
|
430 |
+
if change['type'] == 'UPDATE_FILE' or change['type'] == 'CREATE_FILE':
|
431 |
+
file_path_in_repo = change['path'].lstrip('/').replace(os.sep, '/')
|
432 |
+
if not file_path_in_repo:
|
433 |
+
status_messages.append(f"Skipping {change['type']} operation: empty path.")
|
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}")
|
454 |
+
|
455 |
+
|
456 |
+
elif change['type'] == 'DELETE_FILE':
|
457 |
+
file_path_in_repo = change['path'].lstrip('/').replace(os.sep, '/')
|
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.")
|
532 |
+
logger.info("No file changes to commit.")
|
|
|
533 |
|
534 |
|
535 |
+
finally:
|
536 |
+
# Clean up temporary directory
|
537 |
+
if temp_dir:
|
538 |
+
try:
|
539 |
+
temp_dir.cleanup()
|
540 |
+
logger.info("Cleaned up temporary staging directory.")
|
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
|
553 |
+
api.update_repo_visibility(repo_id=target_repo_id, private=change['private'], repo_type='space')
|
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
|
615 |
+
logger.info(f"Attempting manual file deletion for '{file_path_in_repo}' from {repo_id_for_error_logging}")
|
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
|
|
|
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)
|
|
|
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
|
727 |
+
logger.info(f"Fetching runtime status for Space: {repo_id_for_error_logging}")
|
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}"
|
|
|
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
|
|
|
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
|
|
|
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)
|
794 |
+
if err or not token:
|
795 |
+
logger.error(f"Token error setting privacy: {err or 'Token not found'}")
|
796 |
+
return f"Error getting token: {err or 'Token not found.'}"
|
797 |
+
api = HfApi(token=token)
|
798 |
+
api.update_repo_visibility(repo_id=repo_id, private=private, repo_type='space')
|
799 |
+
logger.info(f"Successfully set privacy for {repo_id} to {private}.")
|
800 |
+
return f"Successfully set privacy for `{repo_id}` to `{private}`."
|
801 |
+
except HfHubHTTPError as e_http:
|
802 |
+
logger.error(f"HTTP error setting privacy for {repo_id}: {e_http}")
|
803 |
+
status_code = e_http.response.status_code if e_http.response else 'N/A'
|
804 |
+
return f"HTTP Error ({status_code}) setting privacy for `{repo_id}`: {e_http.response.text if e_http.response else str(e_http)}"
|
805 |
+
except Exception as e:
|
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:
|
815 |
+
token, err = _get_api_token(hf_api_key)
|
816 |
+
if err or not token:
|
817 |
+
logger.error(f"Token error deleting space: {err or 'Token not found'}")
|
818 |
+
return f"Error getting token: {err or 'Token not found.'}"
|
819 |
+
api = HfApi(token=token)
|
820 |
+
api.delete_repo(repo_id=repo_id, repo_type='space')
|
821 |
+
logger.warning(f"Successfully deleted space {repo_id}.")
|
822 |
+
return f"Successfully deleted space `{repo_id}`."
|
823 |
+
except HfHubHTTPError as e_http:
|
824 |
+
logger.error(f"HTTP error deleting space {repo_id}: {e_http}")
|
825 |
+
status_code = e_http.response.status_code if e_http.response else 'N/A'
|
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}"
|