broadfield-dev commited on
Commit
63c0586
·
verified ·
1 Parent(s): a8db0a2

Update build_logic.py

Browse files
Files changed (1) hide show
  1. build_logic.py +186 -99
build_logic.py CHANGED
@@ -13,7 +13,6 @@ from huggingface_hub import (
13
  hf_hub_download,
14
  delete_file as hf_delete_file,
15
  HfApi,
16
- duplicate_repo as hf_duplicate_repo,
17
  list_repos as hf_list_repos
18
  )
19
  from huggingface_hub.hf_api import CommitOperationDelete, CommitOperationAdd, CommitOperation
@@ -296,12 +295,16 @@ def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, f
296
  logger.exception(f"Error fetching file content for {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}:")
297
  return None, f"Error fetching file content: {str(e)}"
298
 
299
- def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, changeset):
 
300
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
301
  repo_id = None
302
  status_messages = []
303
 
304
- logger.info(f"Attempting to apply {len(changeset)} staged changes to {repo_id_for_error_logging}")
 
 
 
305
 
306
  try:
307
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
@@ -313,60 +316,6 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
313
 
314
  api = HfApi(token=resolved_api_token)
315
 
316
- # --- Handle Exclusive Actions First ---
317
- exclusive_action = next((c for c in changeset if c['type'] in ['DUPLICATE_SPACE', 'DELETE_SPACE']), None)
318
-
319
- if exclusive_action:
320
- if exclusive_action['type'] == 'DUPLICATE_SPACE':
321
- # This should be handled in the confirm_changes handler to trigger a space load
322
- # Reaching here means the logic in confirm_changes failed to intercept it
323
- status_messages.append("Internal Error: DUPLICATE_SPACE action should have been handled exclusively.")
324
- logger.error("Internal Error: DUPLICATE_SPACE action was passed to apply_staged_changes unexpectedly.")
325
- elif exclusive_action['type'] == 'DELETE_SPACE':
326
- # This should also ideally be handled by confirm_changes for UI state reset, but we can execute it here too
327
- delete_owner = exclusive_action.get('owner') or owner_ui
328
- delete_space = exclusive_action.get('space_name') or space_name_ui
329
- delete_repo_id_target = f"{delete_owner}/{delete_space}" if delete_owner and delete_space else repo_id
330
-
331
- if not delete_repo_id_target:
332
- status_messages.append("DELETE_SPACE Error: Target repo_id not specified.")
333
- elif delete_repo_id_target != repo_id:
334
- status_messages.append(f"DELETE_SPACE Error: AI requested deletion of '{delete_repo_id_target}', but this action is only permitted for the currently loaded space '{repo_id}'. Action blocked.")
335
- logger.warning(f"Blocked DELETE_SPACE action in apply_staged_changes: requested '{delete_repo_id_target}', current '{repo_id}'.")
336
- else:
337
- logger.warning(f"Attempting DESTRUCTIVE DELETE_SPACE action for {delete_repo_id_target}")
338
- try:
339
- api.delete_repo(repo_id=delete_repo_id_target, repo_type='space')
340
- status_messages.append(f"DELETE_SPACE: Successfully deleted space `{delete_repo_id_target}`.")
341
- logger.warning(f"Successfully deleted space {delete_repo_id_target}.")
342
- except HfHubHTTPError as e_http:
343
- 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.")
344
- logger.error(f"HTTP error deleting space {delete_repo_id_target}: {e_http}")
345
- except Exception as e:
346
- status_messages.append(f"DELETE_SPACE Error: {str(e)}. Check logs.")
347
- logger.exception(f"Error deleting space {delete_repo_id_target}:")
348
- # If an exclusive action was found and potentially processed, stop here
349
- final_status = " | ".join(status_messages) if status_messages else "Exclusive operation attempted."
350
- logger.info(f"Exclusive action processed. Final status: {final_status}")
351
- return final_status
352
-
353
-
354
- # --- Handle Non-Exclusive Actions and File Changes ---
355
- # This block is only reached if no exclusive action was found
356
-
357
- create_space_op = next((c for c in changeset if c['type'] == 'CREATE_SPACE'), None)
358
- if create_space_op:
359
- try:
360
- 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)
361
- status_messages.append(f"CREATE_SPACE: Successfully created or ensured space [{repo_id}](https://huggingface.co/spaces/{repo_id}) exists.")
362
- logger.info(f"Successfully created or ensured space {repo_id} exists.")
363
- except HfHubHTTPError as e_http:
364
- 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.")
365
- logger.error(f"HTTP error creating space {repo_id}: {e_http}")
366
- except Exception as e:
367
- status_messages.append(f"CREATE_SPACE Error: {e}")
368
- logger.error(f"Error creating space {repo_id}: {e}")
369
-
370
  temp_dir = None
371
  paths_to_upload = {}
372
  delete_operations = []
@@ -386,7 +335,7 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
386
  logger.warning(f"Could not stage .gitattributes: {e}")
387
 
388
 
389
- for change in changeset:
390
  if change['type'] == 'UPDATE_FILE' or change['type'] == 'CREATE_FILE':
391
  file_path_in_repo = change['path'].lstrip('/').replace(os.sep, '/')
392
  if not file_path_in_repo:
@@ -429,7 +378,8 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
429
  repo_id=repo_id,
430
  repo_type="space",
431
  operations=delete_operations,
432
- commit_message=commit_message_delete
 
433
  )
434
  status_messages.append(f"File Deletions: Successfully committed {len(delete_operations)} deletions.")
435
  logger.info("Delete commit successful.")
@@ -453,6 +403,7 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
453
  repo_type="space",
454
  commit_message=commit_message_upload,
455
  allow_patterns=["*"],
 
456
  )
457
  status_messages.append(f"File Uploads/Updates: Successfully uploaded/updated {len(paths_to_upload)} files.")
458
  logger.info("Upload/Update commit successful.")
@@ -476,34 +427,15 @@ def apply_staged_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, cha
476
  except Exception as e:
477
  logger.error(f"Error cleaning up temp dir: {e}")
478
 
479
- for change in changeset:
480
- if change['type'] == 'SET_PRIVACY':
481
- try:
482
- target_repo_id = change.get('repo_id', repo_id)
483
- if not target_repo_id:
484
- status_messages.append("SET_PRIVACY Error: Target repo_id not specified.")
485
- continue
486
- api.update_repo_visibility(repo_id=target_repo_id, private=change['private'], repo_type='space')
487
- status_messages.append(f"SET_PRIVACY: Successfully set `{target_repo_id}` to `private={change['private']}`.")
488
- logger.info(f"Successfully set privacy for {target_repo_id} to {change['private']}.")
489
- except HfHubHTTPError as e_http:
490
- 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.")
491
- logger.error(f"HTTP error setting privacy for {target_repo_id}: {e_http}")
492
- except Exception as e:
493
- status_messages.append(f"SET_PRIVACY Error: {str(e)}. Check logs.")
494
- logger.exception(f"Error setting privacy for {target_repo_id}:")
495
-
496
- # Note: DELETE_SPACE and DUPLICATE_SPACE are handled as exclusive actions at the top
497
-
498
  except HfHubHTTPError as e_http:
499
- logger.error(f"Top-level HTTP error during apply_staged_changes for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
500
  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)}")
501
  except Exception as e:
502
- logger.exception(f"Top-level error during apply_staged_changes for {repo_id_for_error_logging or 'unknown repo'}:")
503
- status_messages.append(f"An unexpected error occurred during apply staged changes: {str(e)}")
504
 
505
- final_status = " | ".join(status_messages) if status_messages else "No operations were applied."
506
- logger.info(f"Finished applying staged changes. Final status: {final_status}")
507
  return final_status
508
 
509
  def delete_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, commit_message_ui=None):
@@ -700,6 +632,172 @@ def build_logic_delete_space(hf_api_key, owner, space_name):
700
  logger.exception(f"Error deleting space {repo_id}:")
701
  return f"Error deleting space `{repo_id}`: {e}"
702
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
  def duplicate_space(hf_api_key, source_repo_id, target_repo_id, private: bool = False):
704
  """Duplicates a Hugging Face Space."""
705
  logger.info(f"Attempting to duplicate '{source_repo_id}' to '{target_repo_id}' (private={private}).")
@@ -709,33 +807,29 @@ def duplicate_space(hf_api_key, source_repo_id, target_repo_id, private: bool =
709
  logger.error(f"Token error duplicating space: {err or 'Token not found'}")
710
  return f"Error getting token: {err or 'Token not found.'}"
711
 
712
- # Validate target_repo_id format if it includes owner, extract owner/name
713
  if '/' in target_repo_id:
714
  target_owner, target_space_name = target_repo_id.split('/', 1)
715
  if not target_owner or not target_space_name or '/' in target_space_name:
716
  return f"Error: Invalid target repository ID format '{target_repo_id}'. Must be '<owner>/<space_name>'."
717
  else:
718
- # If only space name is provided, try to use the token's user as owner
719
  target_space_name = target_repo_id
720
  try:
721
  user_info = whoami(token=token)
722
  target_owner = user_info.get('name')
723
  if not target_owner: raise Exception("Could not determine owner from token.")
724
- target_repo_id = f"{target_owner}/{target_space_name}" # Update target_repo_id
725
  except Exception as e:
726
  logger.error(f"Could not determine target owner from token: {e}")
727
  return f"Error: Target repository ID '{target_repo_id}' is missing owner, and owner could not be determined from token ({e}). Use '<owner>/<space_name>' format or set the Owner field."
728
 
729
-
730
- hf_duplicate_repo(
731
  from_repo=source_repo_id,
732
  to_repo=target_repo_id,
733
  repo_type="space",
734
  token=token,
735
  private=private,
736
- # Use exist_ok=True? The UI should probably warn, but the backend can handle overwrite
737
- # For now, let's assume overwrite is intended if triggered by AI/manual button after warning
738
- exist_ok=True # Allow overwriting existing target space
739
  )
740
  logger.info(f"Successfully duplicated space from {source_repo_id} to {target_repo_id}.")
741
  return f"Successfully duplicated space from `{source_repo_id}` to `{target_repo_id}`."
@@ -756,8 +850,6 @@ def list_user_spaces(hf_api_key, owner=None):
756
  logger.error(f"Token error listing spaces: {err or 'Token not found'}")
757
  return None, f"Error getting token: {err or 'Token not found.'}"
758
 
759
- # If owner is not provided, list spaces for the authenticated user
760
- # We need the username for list_repos filter if owner is None in UI
761
  effective_owner = owner
762
  if not effective_owner:
763
  try:
@@ -767,14 +859,10 @@ def list_user_spaces(hf_api_key, owner=None):
767
  logger.info(f"Listing spaces for auto-detected owner: {effective_owner}")
768
  except Exception as e:
769
  logger.error(f"Could not determine owner from token for listing: {e}")
770
- # Continue trying list_repos without username filter? No, list_repos
771
- # typically needs user or org specified for filtering unless it's public repos.
772
- # Let's require owner or a valid token for user listing.
773
  return None, f"Error auto-detecting owner for listing: {e}. Please specify Owner field."
774
 
775
-
776
  api = HfApi(token=token)
777
- spaces = hf_list_repos(author=effective_owner, type="space", token=token, timeout=20)
778
  space_ids = [f"{r.author}/{r.id}" for r in spaces]
779
 
780
  logger.info(f"Successfully listed {len(space_ids)} spaces for {effective_owner}.")
@@ -784,7 +872,6 @@ def list_user_spaces(hf_api_key, owner=None):
784
  logger.error(f"HTTP error listing spaces for {owner or 'authenticated user'}: {e_http}")
785
  status_code = e_http.response.status_code if e_http.response else 'N/A'
786
  if status_code == 404:
787
- # 404 could mean the owner doesn't exist or has no public spaces and token doesn't give access
788
  return [], f"HTTP Error ({status_code}): Owner '{owner}' not found or has no accessible spaces."
789
  if status_code in (401, 403):
790
  return [], f"HTTP Error ({status_code}): Access denied or authentication required for listing spaces for '{owner}'. Check token permissions."
 
13
  hf_hub_download,
14
  delete_file as hf_delete_file,
15
  HfApi,
 
16
  list_repos as hf_list_repos
17
  )
18
  from huggingface_hub.hf_api import CommitOperationDelete, CommitOperationAdd, CommitOperation
 
295
  logger.exception(f"Error fetching file content for {file_path_in_repo} from {repo_id_for_error_logging or 'unknown repo'}:")
296
  return None, f"Error fetching file content: {str(e)}"
297
 
298
+ # Renamed and modified to only handle file changes (CREATE, UPDATE, DELETE)
299
+ def apply_staged_file_changes(ui_api_token_from_textbox, owner_ui, space_name_ui, file_changeset):
300
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
301
  repo_id = None
302
  status_messages = []
303
 
304
+ logger.info(f"Attempting to apply {len(file_changeset)} staged file changes to {repo_id_for_error_logging}")
305
+
306
+ if not owner_ui or not space_name_ui:
307
+ return "Error: Cannot apply file changes. Owner and Space Name must be provided."
308
 
309
  try:
310
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
 
316
 
317
  api = HfApi(token=resolved_api_token)
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  temp_dir = None
320
  paths_to_upload = {}
321
  delete_operations = []
 
335
  logger.warning(f"Could not stage .gitattributes: {e}")
336
 
337
 
338
+ for change in file_changeset: # Iterate only through file changes
339
  if change['type'] == 'UPDATE_FILE' or change['type'] == 'CREATE_FILE':
340
  file_path_in_repo = change['path'].lstrip('/').replace(os.sep, '/')
341
  if not file_path_in_repo:
 
378
  repo_id=repo_id,
379
  repo_type="space",
380
  operations=delete_operations,
381
+ commit_message=commit_message_delete,
382
+ timeout=30
383
  )
384
  status_messages.append(f"File Deletions: Successfully committed {len(delete_operations)} deletions.")
385
  logger.info("Delete commit successful.")
 
403
  repo_type="space",
404
  commit_message=commit_message_upload,
405
  allow_patterns=["*"],
406
+ timeout=120 # Increased timeout for uploads
407
  )
408
  status_messages.append(f"File Uploads/Updates: Successfully uploaded/updated {len(paths_to_upload)} files.")
409
  logger.info("Upload/Update commit successful.")
 
427
  except Exception as e:
428
  logger.error(f"Error cleaning up temp dir: {e}")
429
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  except HfHubHTTPError as e_http:
431
+ logger.error(f"Top-level HTTP error during apply_staged_file_changes for {repo_id_for_error_logging or 'unknown repo'}: {e_http}")
432
  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)}")
433
  except Exception as e:
434
+ logger.exception(f"Top-level error during apply_staged_file_changes for {repo_id_for_error_logging or 'unknown repo'}:")
435
+ status_messages.append(f"An unexpected error occurred during apply file changes: {str(e)}")
436
 
437
+ final_status = " | ".join(status_messages) if status_messages else "No file operations were applied."
438
+ logger.info(f"Finished applying staged file changes. Final status: {final_status}")
439
  return final_status
440
 
441
  def delete_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, commit_message_ui=None):
 
632
  logger.exception(f"Error deleting space {repo_id}:")
633
  return f"Error deleting space `{repo_id}`: {e}"
634
 
635
+ def build_logic_create_pull_request(hf_api_key, source_repo_id, target_repo_id, title, body=""):
636
+ logger.info(f"Attempting to create PR from '{source_repo_id}' to '{target_repo_id}'. Title: '{title}'")
637
+ try:
638
+ token, err = _get_api_token(hf_api_key)
639
+ if err or not token:
640
+ logger.error(f"Token error creating PR: {err or 'Token not found'}")
641
+ return f"Error getting token: {err or 'Token not found.'}"
642
+
643
+ api = HfApi(token=token)
644
+
645
+ # Assuming the source repo is a Space and the target can be any repo type
646
+ # Assuming PR is from source_repo_id main branch to target_repo_id main branch
647
+ # This might need refinement based on actual use cases (e.g., PR from space branch to model repo main)
648
+ # For simplicity now, assume PR from source_repo_id (space) 'main' to target_repo_id 'main'
649
+ # Need to check if the token has write access to target_repo_id.
650
+
651
+ # A PR is created on the *target* repository
652
+ # We need to check if the token can write to the target.
653
+ # There isn't a direct "check write permission" API, so we rely on create_pull_request errors.
654
+
655
+ pr_url = api.create_pull_request(
656
+ repo_id=target_repo_id, # PR is created ON the target repo
657
+ title=title,
658
+ description=body,
659
+ # Source branch is the branch *in the source repo* (the space)
660
+ # Target branch is the branch *in the target repo*
661
+ # By default, create_pull_request assumes 'main' for both,
662
+ # but source is interpreted as 'main' in the *source_repo_id*
663
+ # and target as 'main' in the *repo_id* parameter (target_repo_id).
664
+ # The API docs mention `repo_id` is the target and `base_repo` is the source for cross-repo PRs.
665
+ # Let's use `repo_id` as target and `base_repo` as source (the space).
666
+ base_repo=source_repo_id, # The Space repo is the source
667
+ base="main", # Source branch in the Space (source_repo_id)
668
+ head="main", # Target branch in the target_repo_id
669
+ token=token,
670
+ timeout=30
671
+ )
672
+
673
+ logger.info(f"Successfully created PR: {pr_url}")
674
+ return f"Successfully created Pull Request: {pr_url}"
675
+
676
+ except HfHubHTTPError as e_http:
677
+ logger.error(f"HTTP error creating PR from {source_repo_id} to {target_repo_id}: {e_http}")
678
+ status_code = e_http.response.status_code if e_http.response else 'N/A'
679
+ # Check for common errors like permission denied or target not found
680
+ if status_code in (401, 403):
681
+ return f"PR Error ({status_code}): Access denied or authentication required to create PR on '{target_repo_id}'. Check token permissions."
682
+ if status_code == 404:
683
+ return f"PR Error ({status_code}): Target repository '{target_repo_id}' not found."
684
+ # Add more specific error checks if needed (e.g., PR already exists, invalid branches)
685
+ if e_http.response and 'already exists' in e_http.response.text:
686
+ return f"PR Error: Pull Request already exists."
687
+ return f"PR HTTP Error ({status_code}): {e_http.response.text if e_http.response else str(e_http)}"
688
+
689
+ except Exception as e:
690
+ logger.exception(f"Error creating PR from {source_repo_id} to {target_repo_id}:")
691
+ return f"PR Error: {e}"
692
+
693
+
694
+ def build_logic_add_comment(hf_api_key, repo_id, comment_text):
695
+ logger.info(f"Attempting to add comment to '{repo_id}'. Text: '{comment_text[:50]}...'")
696
+ try:
697
+ token, err = _get_api_token(hf_api_key)
698
+ if err or not token:
699
+ logger.error(f"Token error adding comment: {err or 'Token not found'}")
700
+ return f"Error getting token: {err or 'Token not found.'}"
701
+
702
+ api = HfApi(token=token)
703
+ # The create_discussion method can be used for comments on the repo itself
704
+ api.create_discussion(
705
+ repo_id=repo_id,
706
+ title=f"Comment from AI Space Commander [{time.strftime('%Y-%m-%d %H:%M')}]", # Title required, use timestamp
707
+ description=comment_text,
708
+ token=token,
709
+ timeout=20
710
+ )
711
+ # Note: This creates a *discussion* rather than a direct comment stream entry.
712
+ # A more direct comment API might exist or could be simulated via other means,
713
+ # but create_discussion is the closest public API method for adding arbitrary text.
714
+ # Let's clarify this limitation or use a different method if available (e.g., adding a commit comment).
715
+ # HfApi does not seem to expose a simple "add comment to repo page" API.
716
+ # create_discussion is the most reasonable public function.
717
+ # Re-reading the API docs... maybe there's no public API for the little comment boxes?
718
+ # The Discussions API is the closest. Let's use that, but inform the user it's a discussion.
719
+ # Or, maybe the AI should be suggesting adding a comment via a COMMIT message?
720
+ # The prompt asks for "send comment", which implies a direct comment stream.
721
+ # This might require an internal API or simulating via another method.
722
+ # Let's stick to the most direct public API for now: Discussions.
723
+ # Or, better, tell the AI this action is not supported via public API or needs clarification.
724
+ # Let's return an error message for now, as `create_discussion` isn't what the user likely means by "add comment".
725
+ # Okay, let's re-read the original codebase. There *was* no comment/like function. The user is *requesting* them.
726
+ # The simplest interpretation of "add comment" is adding to the discussions tab or commit comments.
727
+ # Let's add `create_discussion` for now as it's a public API and the closest fit.
728
+ # We should clarify the AI prompt/action description to mention it creates a *discussion*.
729
+ # Reverted: create_discussion requires a *title*. The prompt just says "comment text".
730
+ # This reinforces that the requested action might not map directly to a public API.
731
+ # Let's implement it using `create_discussion` and add a default title.
732
+
733
+ api.create_discussion(
734
+ repo_id=repo_id,
735
+ title=f"AI Space Commander Comment: {comment_text[:50]}{'...' if len(comment_text) > 50 else ''} [{time.strftime('%Y-%m-%d %H:%M')}]", # Use part of comment + timestamp as title
736
+ description=comment_text, # Full comment in description
737
+ token=token,
738
+ timeout=20
739
+ )
740
+
741
+ logger.info(f"Successfully added comment (as discussion) to {repo_id}.")
742
+ return f"Successfully added comment (as a discussion) to `{repo_id}`."
743
+
744
+ except HfHubHTTPError as e_http:
745
+ logger.error(f"HTTP error adding comment to {repo_id}: {e_http}")
746
+ status_code = e_http.response.status_code if e_http.response else 'N/A'
747
+ if status_code in (401, 403):
748
+ return f"Comment Error ({status_code}): Access denied or authentication required to add comment on '{repo_id}'. Check token permissions."
749
+ if status_code == 404:
750
+ return f"Comment Error ({status_code}): Repository '{repo_id}' not found."
751
+ return f"Comment HTTP Error ({status_code}): {e_http.response.text if e_http.response else str(e_http)}"
752
+
753
+ except Exception as e:
754
+ logger.exception(f"Error adding comment to {repo_id}:")
755
+ return f"Comment Error: {e}"
756
+
757
+
758
+ def build_logic_like_space(hf_api_key, repo_id):
759
+ logger.info(f"Attempting to like space '{repo_id}'.")
760
+ try:
761
+ token, err = _get_api_token(hf_api_key)
762
+ if err or not token:
763
+ logger.error(f"Token error liking space: {err or 'Token not found'}")
764
+ return f"Error getting token: {err or 'Token not found.'}"
765
+
766
+ api = HfApi(token=token)
767
+ # HfApi does not have a direct 'like' method.
768
+ # This action might also require an internal API or not be publicly exposed.
769
+ # Let's return an error for now, or note it's not directly supported.
770
+ # Given the request "add... like", it implies it *should* be possible.
771
+ # A manual approach might involve an authenticated API call not wrapped by hf_hub.
772
+ # For now, let's return an informative error.
773
+ # Reverted: There *is* an internal endpoint used by the UI. It's not in the public `HfApi`.
774
+ # Implementing this reliably without internal API knowledge is difficult and fragile.
775
+ # Let's return an error message stating it's not supported via the public API.
776
+
777
+ return f"Like Error: Liking spaces is not directly supported via the public Hugging Face Hub API used by this tool."
778
+
779
+ # Example (likely unstable/unsupported) attempt using requests if we knew the endpoint:
780
+ # like_url = f"https://huggingface.co/api/v1/repos/{repo_id}/like"
781
+ # headers = {"Authorization": f"Bearer {token}"}
782
+ # response = requests.post(like_url, headers=headers, timeout=10)
783
+ # response.raise_for_status()
784
+ # logger.info(f"Successfully liked space: {repo_id}")
785
+ # return f"Successfully liked space: `{repo_id}`."
786
+ # except requests.exceptions.HTTPError as e:
787
+ # # Handle 409 Conflict (already liked) or other errors
788
+ # status_code = e.response.status_code if e.response else 'N/A'
789
+ # if status_code == 409: return f"Like Error: Space '{repo_id}' already liked."
790
+ # return f"Like HTTP Error ({status_code}): {e.response.text if e.response else str(e)}"
791
+ # except Exception as e:
792
+ # logger.exception(f"Error liking space {repo_id}:")
793
+ # return f"Like Error: {e}"
794
+
795
+
796
+ except Exception as e: # Catch potential errors even in the error path above
797
+ logger.exception(f"Unexpected error in build_logic_like_space for {repo_id}:")
798
+ return f"Like Error: An unexpected error occurred: {e}"
799
+
800
+
801
  def duplicate_space(hf_api_key, source_repo_id, target_repo_id, private: bool = False):
802
  """Duplicates a Hugging Face Space."""
803
  logger.info(f"Attempting to duplicate '{source_repo_id}' to '{target_repo_id}' (private={private}).")
 
807
  logger.error(f"Token error duplicating space: {err or 'Token not found'}")
808
  return f"Error getting token: {err or 'Token not found.'}"
809
 
 
810
  if '/' in target_repo_id:
811
  target_owner, target_space_name = target_repo_id.split('/', 1)
812
  if not target_owner or not target_space_name or '/' in target_space_name:
813
  return f"Error: Invalid target repository ID format '{target_repo_id}'. Must be '<owner>/<space_name>'."
814
  else:
 
815
  target_space_name = target_repo_id
816
  try:
817
  user_info = whoami(token=token)
818
  target_owner = user_info.get('name')
819
  if not target_owner: raise Exception("Could not determine owner from token.")
820
+ target_repo_id = f"{target_owner}/{target_space_name}"
821
  except Exception as e:
822
  logger.error(f"Could not determine target owner from token: {e}")
823
  return f"Error: Target repository ID '{target_repo_id}' is missing owner, and owner could not be determined from token ({e}). Use '<owner>/<space_name>' format or set the Owner field."
824
 
825
+ api = HfApi(token=token) # Use HfApi object to call duplicate_repo
826
+ api.duplicate_repo(
827
  from_repo=source_repo_id,
828
  to_repo=target_repo_id,
829
  repo_type="space",
830
  token=token,
831
  private=private,
832
+ exist_ok=True
 
 
833
  )
834
  logger.info(f"Successfully duplicated space from {source_repo_id} to {target_repo_id}.")
835
  return f"Successfully duplicated space from `{source_repo_id}` to `{target_repo_id}`."
 
850
  logger.error(f"Token error listing spaces: {err or 'Token not found'}")
851
  return None, f"Error getting token: {err or 'Token not found.'}"
852
 
 
 
853
  effective_owner = owner
854
  if not effective_owner:
855
  try:
 
859
  logger.info(f"Listing spaces for auto-detected owner: {effective_owner}")
860
  except Exception as e:
861
  logger.error(f"Could not determine owner from token for listing: {e}")
 
 
 
862
  return None, f"Error auto-detecting owner for listing: {e}. Please specify Owner field."
863
 
 
864
  api = HfApi(token=token)
865
+ spaces = api.list_repos(author=effective_owner, type="space", token=token, timeout=20)
866
  space_ids = [f"{r.author}/{r.id}" for r in spaces]
867
 
868
  logger.info(f"Successfully listed {len(space_ids)} spaces for {effective_owner}.")
 
872
  logger.error(f"HTTP error listing spaces for {owner or 'authenticated user'}: {e_http}")
873
  status_code = e_http.response.status_code if e_http.response else 'N/A'
874
  if status_code == 404:
 
875
  return [], f"HTTP Error ({status_code}): Owner '{owner}' not found or has no accessible spaces."
876
  if status_code in (401, 403):
877
  return [], f"HTTP Error ({status_code}): Access denied or authentication required for listing spaces for '{owner}'. Check token permissions."