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