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