opex792 commited on
Commit
8f3e9cf
·
verified ·
1 Parent(s): 486885c

Upload 4 files

Browse files
Files changed (3) hide show
  1. app.py +83 -4
  2. app_logic.py +6 -246
  3. requirements.txt +3 -1
app.py CHANGED
@@ -1,4 +1,10 @@
 
 
1
  import gradio as gr
 
 
 
 
2
  from app_logic import (
3
  create_space,
4
  update_space_file,
@@ -6,9 +12,77 @@ from app_logic import (
6
  KEYLOCK_DECODE_AVAILABLE,
7
  list_space_files_for_browsing,
8
  get_space_file_content,
 
9
  )
10
- KEYLOCK_DECODE_AVAILABLE = False
11
- # Gradio interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  def main_ui():
13
  with gr.Blocks(theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky), title="Hugging Face Space Builder") as demo:
14
  gr.Markdown(
@@ -242,6 +316,11 @@ Generate program files for a project as a single plain text string, strictly adh
242
  )
243
  return demo
244
 
 
 
 
 
 
245
  if __name__ == "__main__":
246
- demo = main_ui()
247
- demo.launch(mcp_server=True,show_error=True)
 
1
+
2
+
3
  import gradio as gr
4
+ from fastapi import FastAPI, HTTPException, Body
5
+ from pydantic import BaseModel
6
+ from typing import Optional
7
+
8
  from app_logic import (
9
  create_space,
10
  update_space_file,
 
12
  KEYLOCK_DECODE_AVAILABLE,
13
  list_space_files_for_browsing,
14
  get_space_file_content,
15
+ delete_space as logic_delete_space
16
  )
17
+
18
+ # --- FastAPI App Initialization ---
19
+ app = FastAPI(
20
+ title="Hugging Face Space Builder API",
21
+ description="API to create, manage, and delete Hugging Face Spaces.",
22
+ version="1.0.0",
23
+ )
24
+
25
+ # --- Pydantic Models for API ---
26
+ class SpaceCreate(BaseModel):
27
+ api_token: str
28
+ space_name: str
29
+ owner: Optional[str] = None
30
+ sdk: str = "gradio"
31
+ markdown_content: str
32
+
33
+ class SpaceUpdate(BaseModel):
34
+ api_token: str
35
+ space_name: str
36
+ owner: Optional[str] = None
37
+ filepath: str
38
+ content: str
39
+ commit_message: str
40
+
41
+ class SpaceDelete(BaseModel):
42
+ api_token: str
43
+ space_name: str
44
+ owner: Optional[str] = None
45
+
46
+ # --- API Endpoints ---
47
+ @app.post("/spaces/create", summary="Create a new Hugging Face Space")
48
+ def api_create_space(space_data: SpaceCreate):
49
+ result = create_space(
50
+ space_data.api_token,
51
+ space_data.space_name,
52
+ space_data.owner,
53
+ space_data.sdk,
54
+ space_data.markdown_content
55
+ )
56
+ if "Error" in result:
57
+ raise HTTPException(status_code=400, detail=result)
58
+ return {"message": result}
59
+
60
+ @app.put("/spaces/update_file", summary="Update a file in a Hugging Face Space")
61
+ def api_update_space_file(update_data: SpaceUpdate):
62
+ result = update_space_file(
63
+ update_data.api_token,
64
+ update_data.space_name,
65
+ update_data.owner,
66
+ update_data.filepath,
67
+ update_data.content,
68
+ update_data.commit_message
69
+ )
70
+ if "Error" in result:
71
+ raise HTTPException(status_code=400, detail=result)
72
+ return {"message": result}
73
+
74
+ @app.delete("/spaces/delete", summary="Delete a Hugging Face Space")
75
+ def api_delete_space(delete_data: SpaceDelete):
76
+ result = logic_delete_space(
77
+ delete_data.api_token,
78
+ delete_data.space_name,
79
+ delete_data.owner
80
+ )
81
+ if "Error" in result:
82
+ raise HTTPException(status_code=400, detail=result)
83
+ return {"message": result}
84
+
85
+ # --- Gradio UI ---
86
  def main_ui():
87
  with gr.Blocks(theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky), title="Hugging Face Space Builder") as demo:
88
  gr.Markdown(
 
316
  )
317
  return demo
318
 
319
+ # --- Mount Gradio UI to FastAPI ---
320
+ demo = main_ui()
321
+ app = gr.mount_gradio_app(app, demo, path="/")
322
+
323
+ # --- Main Execution ---
324
  if __name__ == "__main__":
325
+ import uvicorn
326
+ uvicorn.run(app, host="0.0.0.0", port=7860)
app_logic.py CHANGED
@@ -1,237 +1,5 @@
1
- import os
2
- import re
3
- import tempfile
4
- import shutil
5
- import git
6
- import re
7
 
8
- from huggingface_hub import (
9
- create_repo,
10
- upload_folder,
11
- list_repo_files,
12
- Repository,
13
- whoami,
14
- hf_hub_download, # New import
15
- )
16
- import logging
17
- from pathlib import Path
18
- from PIL import Image
19
-
20
- try:
21
- from keylock_decode import decode_from_image_pil
22
- KEYLOCK_DECODE_AVAILABLE = True
23
- except ImportError:
24
- KEYLOCK_DECODE_AVAILABLE = False
25
- decode_from_image_pil = None
26
- logging.warning("keylock-decode library not found. KeyLock Wallet image feature will be disabled.")
27
-
28
- logging.basicConfig(
29
- level=logging.INFO,
30
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
31
- )
32
- logger = logging.getLogger(__name__)
33
-
34
- # --- Helper Function to Get API Token (Unchanged) ---
35
- def _get_api_token(ui_token_from_textbox=None):
36
- env_token = os.getenv('HF_TOKEN')
37
- if env_token: return env_token, None
38
- if ui_token_from_textbox: return ui_token_from_textbox, None
39
- return None, "Error: Hugging Face API token not provided."
40
-
41
- # --- `load_token_from_image_and_set_env` (Unchanged - Terminology and debug logic as before) ---
42
- def load_token_from_image_and_set_env(image_pil_object: Image.Image, password: str):
43
- if not KEYLOCK_DECODE_AVAILABLE: return "Error: KeyLock-Decode library is not installed."
44
- if image_pil_object is None: return "Error: No KeyLock Wallet image provided."
45
- if not password: return "Error: Password cannot be empty."
46
- status_messages_display = []
47
- # ... (rest of the function, ensure debug logic is as intended or removed)
48
- try:
49
- logger.info(f"Attempting to decode from KeyLock Wallet image...")
50
- decoded_data, status_msgs_from_lib = decode_from_image_pil(image_pil_object, password, set_environment_variables=True)
51
- status_messages_display.extend(status_msgs_from_lib)
52
- if decoded_data:
53
- status_messages_display.append("\n**Decoded Data Summary (sensitive values masked):**")
54
- for key, value in decoded_data.items():
55
- display_value = '********' if any(k_word in key.upper() for k_word in ['TOKEN', 'KEY', 'SECRET', 'PASS']) else value
56
- status_messages_display.append(f"- {key}: {display_value}")
57
- if os.getenv('HF_TOKEN'): status_messages_display.append(f"\n**SUCCESS: HF_TOKEN set from KeyLock Wallet image.**")
58
- # ... (other status messages)
59
- except ValueError as e: status_messages_display.append(f"**Decoding Error:** {e}")
60
- except Exception as e: status_messages_display.append(f"**Unexpected decoding error:** {str(e)}")
61
- return "\n".join(status_messages_display)
62
-
63
-
64
-
65
- '''def process_commented_markdown(commented_input):
66
- """Process a commented markdown string by stripping '# ' from each line if '# # Space:' is present."""
67
- lines = commented_input.strip().split("\n")
68
- print(type(lines))
69
- # Check for '# # Space:' or variations (e.g., '# Space:') in any line
70
- if any( "# # Space:" in line.strip() for line in lines):
71
- print("YES")
72
- cleaned_lines = [line.lstrip("# ") for line in lines]
73
- return cleaned_lines
74
- return lines'''
75
-
76
- def process_commented_markdown(commented_input):
77
- """Process a commented markdown string by stripping '# ' from each line if '# # Space:' is present."""
78
- lines = commented_input.strip().split("\n")
79
- print(type(lines)) # Original debug print
80
- # Check for '# # Space:' or variations (e.g., '# Space:') in any line
81
- if any( "# # Space:" in line.strip() for line in lines):
82
- print("YES") # Original debug print
83
-
84
- cleaned_lines = [line[2:] if line.startswith("# ") else line for line in lines]
85
-
86
- return cleaned_lines
87
- return lines
88
-
89
- def parse_markdown(markdown_input):
90
- space_info = {"repo_name_md": "", "owner_md": "", "files": []}
91
- current_file_path = None; current_file_content_lines = []
92
- in_file_definition = False; in_code_block = False
93
- print(markdown_input)
94
- lines = process_commented_markdown(markdown_input)
95
- print(lines)
96
- #lines = markdown_input.strip().split("\n")
97
-
98
- for line_content_orig in lines:
99
- line_content_stripped = line_content_orig.strip()
100
- if line_content_stripped.startswith("### File:"):
101
- if current_file_path and in_file_definition:
102
- space_info["files"].append({"path": current_file_path, "content": "\n".join(current_file_content_lines)})
103
- current_file_path = line_content_stripped.replace("### File:", "").strip()
104
- current_file_content_lines = []
105
- in_file_definition = True; in_code_block = False
106
- continue
107
- if not in_file_definition:
108
- if line_content_stripped.startswith("# Space:"):
109
- full_space_name_md = line_content_stripped.replace("# Space:", "").strip()
110
- if "/" in full_space_name_md: space_info["owner_md"], space_info["repo_name_md"] = full_space_name_md.split("/", 1)
111
- else: space_info["repo_name_md"] = full_space_name_md
112
- continue
113
- if line_content_stripped.startswith("```"):
114
- in_code_block = not in_code_block
115
- continue
116
- current_file_content_lines.append(line_content_orig)
117
- if current_file_path and in_file_definition:
118
- space_info["files"].append({"path": current_file_path, "content": "\n".join(current_file_content_lines)})
119
- space_info["files"] = [f for f in space_info["files"] if f.get("path")]
120
- return space_info
121
-
122
-
123
-
124
- # --- `_determine_repo_id` (Unchanged) ---
125
- def _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui):
126
- if not space_name_ui: return None, "Error: Space Name cannot be empty."
127
- if "/" in space_name_ui: return None, "Error: Space Name should not contain '/'. Use Owner field."
128
- final_owner = owner_ui; error_message = None
129
- if not final_owner:
130
- resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
131
- if token_err: return None, token_err
132
- if not resolved_api_token: return None, "Error: API token required for auto owner determination."
133
- try:
134
- user_info = whoami(token=resolved_api_token)
135
- if user_info and 'name' in user_info: final_owner = user_info['name']
136
- else: error_message = "Error: Could not retrieve username. Check token/permissions or specify Owner."
137
- except Exception as e: error_message = f"Error retrieving username: {str(e)}. Specify Owner."
138
- if error_message: return None, error_message
139
- if not final_owner: return None, "Error: Owner could not be determined."
140
- return f"{final_owner}/{space_name_ui}", None
141
-
142
-
143
- # --- New Function to Fetch File Content from Hub ---
144
- def get_space_file_content(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo):
145
- """Fetches content of a specific file from a Hugging Face Space."""
146
- repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
147
- try:
148
- resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
149
- if token_err:
150
- return None, token_err # Return error as second element for consistency
151
-
152
- repo_id, err = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
153
- if err:
154
- return None, err
155
- repo_id_for_error_logging = repo_id
156
-
157
- if not file_path_in_repo:
158
- return None, "Error: File path cannot be empty."
159
-
160
- logger.info(f"Attempting to download file: {file_path_in_repo} from Space: {repo_id}")
161
- downloaded_file_path = hf_hub_download(
162
- repo_id=repo_id,
163
- filename=file_path_in_repo,
164
- repo_type="space",
165
- token=resolved_api_token,
166
- # revision="main", # Optional: specify a branch/commit
167
- # cache_dir=... # Optional: manage cache
168
- )
169
-
170
- content = Path(downloaded_file_path).read_text(encoding="utf-8")
171
- logger.info(f"Successfully downloaded and read file: {file_path_in_repo} from {repo_id}")
172
- return content, None # Return content and no error
173
-
174
- except Exception as e:
175
- # Catch specific huggingface_hub.utils.HFValidationError for not found etc.
176
- if "404" in str(e) or "not found" in str(e).lower():
177
- logger.warning(f"File not found {file_path_in_repo} in {repo_id_for_error_logging}: {e}")
178
- return None, f"Error: File '{file_path_in_repo}' not found in Space '{repo_id_for_error_logging}'."
179
- logger.exception(f"Error fetching file content for {file_path_in_repo} from {repo_id_for_error_logging}:")
180
- return None, f"Error fetching file content: {str(e)}"
181
-
182
- # --- Function to list files (reused, but now distinct from fetching content) ---
183
- def list_space_files_for_browsing(ui_api_token_from_textbox, space_name_ui, owner_ui):
184
- """Lists files in a Hugging Face Space, returns list or error."""
185
- repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
186
- try:
187
- resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
188
- if token_err: return None, token_err
189
-
190
- repo_id, err = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
191
- if err: return None, err
192
- repo_id_for_error_logging = repo_id
193
-
194
- files = list_repo_files(repo_id=repo_id, token=resolved_api_token, repo_type="space")
195
- if not files:
196
- return [], f"No files found in Space `{repo_id}`." # Return empty list and info message
197
- return files, None # Return list of files and no error
198
- except Exception as e:
199
- logger.exception(f"Error listing files for {repo_id_for_error_logging}:")
200
- return None, f"Error listing files for `{repo_id_for_error_logging}`: {str(e)}"
201
-
202
-
203
- # --- Core Functions: `create_space`, `update_space_file` (Unchanged from previous correct versions) ---
204
- def create_space(ui_api_token_from_textbox, space_name_ui, owner_ui, sdk_ui, markdown_input):
205
- repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
206
- try:
207
- resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
208
- if token_err: return token_err
209
- repo_id, err = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
210
- if err: return err
211
- repo_id_for_error_logging = repo_id
212
- space_info = parse_markdown(markdown_input)
213
- if not space_info["files"]: return "Error: No files found in markdown."
214
- with tempfile.TemporaryDirectory() as temp_dir:
215
- repo_staging_path = Path(temp_dir) / "repo_staging_content"
216
- repo_staging_path.mkdir(exist_ok=True)
217
- for file_info in space_info["files"]:
218
- if not file_info.get("path"): continue
219
- file_path_abs = repo_staging_path / file_info["path"]
220
- file_path_abs.parent.mkdir(parents=True, exist_ok=True)
221
- with open(file_path_abs, "w", encoding="utf-8") as f: f.write(file_info["content"])
222
- try:
223
- create_repo(repo_id=repo_id, token=resolved_api_token, repo_type="space", space_sdk=sdk_ui, private=False)
224
- except Exception as e:
225
- err_str = str(e).lower()
226
- if not ("already exists" in err_str or "you already created this repo" in err_str or "exists" in err_str):
227
- return f"Error creating Space '{repo_id}': {str(e)}"
228
- upload_folder(repo_id=repo_id, folder_path=str(repo_staging_path), path_in_repo=".", token=resolved_api_token, repo_type="space", commit_message=f"Initial Space setup of {repo_id} via Builder")
229
- return f"Successfully created/updated Space: [{repo_id}](https://huggingface.co/spaces/{repo_id})"
230
- except Exception as e:
231
- logger.exception(f"Error in create_space for {repo_id_for_error_logging}:")
232
- return f"Error during Space creation/update: {str(e)}"
233
-
234
- def update_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_path_in_repo, file_content, commit_message_ui):
235
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
236
  try:
237
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
@@ -239,17 +7,9 @@ def update_space_file(ui_api_token_from_textbox, space_name_ui, owner_ui, file_p
239
  repo_id, err = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
240
  if err: return err
241
  repo_id_for_error_logging = repo_id
242
- if not file_path_in_repo: return "Error: File Path to update cannot be empty."
243
- file_path_in_repo = file_path_in_repo.lstrip('/').replace(os.sep, '/')
244
- commit_message_ui = commit_message_ui or f"Update {file_path_in_repo} via Space Builder"
245
- with tempfile.TemporaryDirectory() as temp_dir_for_update:
246
- repo_local_clone_path = Path(temp_dir_for_update) / "update_clone"
247
- cloned_repo = Repository(local_dir=str(repo_local_clone_path), clone_from=f"https://huggingface.co/spaces/{repo_id}", repo_type="space", use_auth_token=resolved_api_token, git_user="Space Builder Bot", git_email="space-builder@huggingface.co")
248
- full_local_file_path = Path(cloned_repo.local_dir) / file_path_in_repo
249
- full_local_file_path.parent.mkdir(parents=True, exist_ok=True)
250
- with open(full_local_file_path, "w", encoding="utf-8") as f: f.write(file_content)
251
- cloned_repo.push_to_hub(commit_message=commit_message_ui)
252
- return f"Successfully updated `{file_path_in_repo}` in Space [{repo_id}](https://huggingface.co/spaces/{repo_id})"
253
  except Exception as e:
254
- logger.exception(f"Error in update_space_file for {repo_id_for_error_logging}, file {file_path_in_repo}:")
255
- return f"Error updating file for `{repo_id_for_error_logging}`: {str(e)}"
 
 
 
 
 
 
 
1
 
2
+ def delete_space(ui_api_token_from_textbox, space_name_ui, owner_ui):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  repo_id_for_error_logging = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
4
  try:
5
  resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
 
7
  repo_id, err = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
8
  if err: return err
9
  repo_id_for_error_logging = repo_id
10
+
11
+ delete_repo(repo_id=repo_id, token=resolved_api_token, repo_type="space")
12
+ return f"Successfully deleted Space: {repo_id}"
 
 
 
 
 
 
 
 
13
  except Exception as e:
14
+ logger.exception(f"Error in delete_space for {repo_id_for_error_logging}:")
15
+ return f"Error deleting space for `{repo_id_for_error_logging}`: {str(e)}"
requirements.txt CHANGED
@@ -1,4 +1,6 @@
1
  gradio
2
  huggingface_hub
3
  gitpython
4
- Pillow
 
 
 
1
  gradio
2
  huggingface_hub
3
  gitpython
4
+ Pillow
5
+ fastapi
6
+ uvicorn