Update build_logic.py
Browse files- 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 |
-
|
|
|
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(
|
|
|
|
|
|
|
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
|
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
|
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
|
503 |
-
status_messages.append(f"An unexpected error occurred during apply
|
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}"
|
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 |
-
|
731 |
from_repo=source_repo_id,
|
732 |
to_repo=target_repo_id,
|
733 |
repo_type="space",
|
734 |
token=token,
|
735 |
private=private,
|
736 |
-
|
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 =
|
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."
|