broadfield-dev commited on
Commit
f89e3c9
·
verified ·
1 Parent(s): 3afa2e6

Update app_logic.py

Browse files
Files changed (1) hide show
  1. app_logic.py +101 -97
app_logic.py CHANGED
@@ -7,7 +7,7 @@ from huggingface_hub import (
7
  create_repo,
8
  upload_folder,
9
  list_repo_files,
10
- # delete_file, # Not used in the provided code, can be removed if not planned
11
  Repository,
12
  whoami,
13
  )
@@ -21,71 +21,86 @@ logger = logging.getLogger(__name__)
21
  # Function to parse markdown input
22
  def parse_markdown(markdown_input):
23
  """Parse markdown input to extract space details and file structure."""
24
- # This function remains largely the same as its file parsing logic wasn't flagged as an issue,
25
- # only its space name/owner extraction's usage.
26
- space_info = {"repo_name_md": "", "owner_md": "", "files": []} # Renamed keys for clarity
27
  current_file = None
28
  file_content = []
29
- in_file_content = False
 
30
 
31
  lines = markdown_input.strip().split("\n")
32
- for line in lines:
33
- # Extract space name from markdown (for informational purposes, not for repo_id creation)
34
- if line.startswith("# Space:"):
35
- full_space_name_md = line.replace("# Space:", "").strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  if "/" in full_space_name_md:
37
  space_info["owner_md"], space_info["repo_name_md"] = full_space_name_md.split("/", 1)
38
  else:
39
  space_info["repo_name_md"] = full_space_name_md
40
- # Detect file structure section
41
- elif line.startswith("## File Structure"):
42
- continue
43
- # Detect file in structure (this part seems less used if "### File:" is primary)
44
- elif line.startswith("📄") or line.startswith("📁"):
45
- if current_file and file_content: # If content was being collected for a previous file (e.g. under "### File:")
 
46
  space_info["files"].append({"path": current_file, "content": "\n".join(file_content)})
47
- elif current_file and not file_content: # Path defined by 📄 but no content under a ### File: block
48
- # This case implies an empty file if no ### File: section follows for it.
49
- # Or, this line could be ignored if ### File: is the true source.
50
- # For now, let's assume 📄 just lists a file, content comes from ### File:
51
- pass # current_file is updated, but content collection relies on ### File: or ```
52
 
53
- current_file = line[2:].strip() # Update current file path from 📄 or 📁
54
- file_content = [] # Reset content for this new file context
55
- in_file_content = False # Content for 📄 files is typically defined by subsequent ### File: blocks
56
- # Detect file content section
57
- elif line.startswith("### File:"):
58
  if current_file and file_content: # Save content of the previous file
59
  space_info["files"].append({"path": current_file, "content": "\n".join(file_content)})
60
 
61
- current_file = line.replace("### File:", "").strip()
62
  file_content = [] # Reset for new file
63
- in_file_content = True # Start collecting content lines
64
- # Handle file content (inside ``` blocks or plain text lines)
65
- elif in_file_content and line.strip().startswith("```"): # Code block fences
66
- if file_content and line.strip() == "```": # Closing fence ```
67
- # Current file_content is the content of the code block
68
- space_info["files"].append({"path": current_file, "content": "\n".join(file_content)})
69
- file_content = []
70
- in_file_content = False # End of this file's content block
71
- current_file = None # Reset current file, expect new ### File: or 📄
72
- elif not file_content and line.strip().startswith("```"): # Opening fence ``` or ```lang
73
- # This line itself is the start of the content if it's like ```python
74
- file_content.append(line)
75
- # in_file_content remains true
76
- else: # Mid-block line that happens to be ```, treat as content
77
- file_content.append(line)
78
- elif in_file_content:
79
- file_content.append(line)
80
 
81
  # Append the last file's content if any
82
  if current_file and file_content:
83
  space_info["files"].append({"path": current_file, "content": "\n".join(file_content)})
84
 
85
- # Filter out any entries that might have been added with no path (shouldn't happen with current logic)
86
- space_info["files"] = [f for f in space_info["files"] if f.get("path")]
87
  return space_info
88
 
 
89
  def _determine_repo_id(api_token, space_name_ui, owner_ui):
90
  """
91
  Determines the final owner and constructs the repo_id.
@@ -95,21 +110,22 @@ def _determine_repo_id(api_token, space_name_ui, owner_ui):
95
  """
96
  if not space_name_ui:
97
  return None, "Error: Space Name cannot be empty."
98
- if "/" in space_name_ui:
99
  return None, "Error: Space Name should not contain '/'. Please use the Owner field for the namespace."
100
 
101
  final_owner = owner_ui
102
  error_message = None
103
 
104
- if not final_owner: # If UI owner field is empty
105
  if not api_token:
106
  return None, "Error: API token is required to automatically determine owner when Owner field is empty."
107
  try:
108
  user_info = whoami(token=api_token)
109
  if user_info and 'name' in user_info:
110
  final_owner = user_info['name']
 
111
  else:
112
- logger.error(f"whoami(token=...) returned: {user_info} when trying to determine owner.")
113
  error_message = "Error: Could not retrieve username from API token. Ensure token is valid and has 'Read profile' permissions. Or, specify Owner manually."
114
  except Exception as e:
115
  logger.error(f"Error calling whoami for owner: {str(e)}")
@@ -134,25 +150,21 @@ def create_space(api_token, space_name_ui, owner_ui, sdk_ui, markdown_input):
134
  if err:
135
  return err
136
 
137
- # Parse markdown input for file structure
138
- # The space_info["repo_name_md"] and space_info["owner_md"] are NOT used for repo_id creation.
139
  space_info = parse_markdown(markdown_input)
140
  if not space_info["files"]:
141
- return "Error: No files found in the markdown input. Ensure '### File: path/to/file.ext' markers are used."
142
 
143
  # Create temporary directory
144
  with tempfile.TemporaryDirectory() as temp_dir:
145
- # Use a generic name for the local repo dir, not space_name_ui, to avoid issues if space_name_ui has special chars
146
- # Or, ensure space_name_ui is filesystem-safe. For simplicity, let's use a fixed name.
147
- repo_local_path = os.path.join(temp_dir, "repo")
148
- os.makedirs(repo_local_path, exist_ok=True)
149
 
150
  # Write files to temporary directory
151
  for file_info in space_info["files"]:
152
  if not file_info.get("path"):
153
  logger.warning(f"Skipping file with no path: {file_info}")
154
  continue
155
- file_path_abs = Path(repo_local_path) / file_info["path"]
156
  file_path_abs.parent.mkdir(parents=True, exist_ok=True)
157
  with open(file_path_abs, "w", encoding="utf-8") as f:
158
  f.write(file_info["content"])
@@ -163,47 +175,40 @@ def create_space(api_token, space_name_ui, owner_ui, sdk_ui, markdown_input):
163
  create_repo(
164
  repo_id=repo_id,
165
  token=api_token,
166
- repo_type="space",
167
  space_sdk=sdk_ui,
168
- private=False, # Or make this an option
169
  )
170
- logger.info(f"Created Space: {repo_id}")
171
  except Exception as e:
172
- if "already exists" in str(e).lower() or "you already created this repo" in str(e).lower() :
173
- logger.info(f"Space {repo_id} already exists, proceeding to update/upload.")
 
174
  else:
175
  return f"Error creating Space '{repo_id}': {str(e)}"
176
-
177
- # Initialize Git repository (optional but good practice before upload_folder if it relies on it)
178
- # repo_git = git.Repo.init(repo_local_path)
179
- # repo_git.git.add(all=True)
180
- # try:
181
- # repo_git.index.commit("Initial commit from Space Builder")
182
- # except git.exc.GitCommandError as e:
183
- # if "nothing to commit" in str(e):
184
- # logger.info("No changes to commit locally.")
185
- # else:
186
- # raise e
187
 
188
  # Push to Hugging Face Space
189
- # upload_folder handles its own commit if the folder is not already a git repo or has uncommitted changes.
190
  upload_folder(
191
  repo_id=repo_id,
192
- folder_path=repo_local_path, # path to the directory containing files
193
- path_in_repo=".", # path where files should be uploaded within the repo
194
  token=api_token,
195
- commit_message=f"Initial Space setup of {repo_id}",
 
 
 
196
  )
197
-
198
  return f"Successfully created/updated Space: [{repo_id}](https://huggingface.co/spaces/{repo_id})"
199
 
200
  except Exception as e:
201
- logger.error(f"Error in create_space: {str(e)}")
202
- return f"Error: {str(e)}"
203
 
204
  # Function to view Space files
205
  def view_space_files(api_token, space_name_ui, owner_ui):
206
  """List files in a Hugging Face Space."""
 
207
  try:
208
  if not api_token:
209
  return "Error: Please provide a valid Hugging Face API token."
@@ -211,19 +216,20 @@ def view_space_files(api_token, space_name_ui, owner_ui):
211
  repo_id, err = _determine_repo_id(api_token, space_name_ui, owner_ui)
212
  if err:
213
  return err
214
-
215
- files = list_repo_files(repo_id=repo_id, token=api_token, repo_type="space")
216
  if files:
217
  return f"Files in `{repo_id}`:\n\n" + "\n".join([f"- `{f}`" for f in files])
218
  else:
219
  return f"No files found in the Space `{repo_id}`."
220
  except Exception as e:
221
- logger.error(f"Error in view_space_files: {str(e)}")
222
- return f"Error listing files for `{repo_id or (owner_ui + '/' + space_name_ui if owner_ui else space_name_ui)}`: {str(e)}"
223
 
224
  # Function to update a Space file
225
  def update_space_file(api_token, space_name_ui, owner_ui, file_path_in_repo, file_content, commit_message_ui):
226
  """Update a file in a Hugging Face Space with a commit."""
 
227
  try:
228
  if not api_token:
229
  return "Error: Please provide a valid Hugging Face API token."
@@ -237,32 +243,30 @@ def update_space_file(api_token, space_name_ui, owner_ui, file_path_in_repo, fil
237
  if not commit_message_ui:
238
  commit_message_ui = f"Update {file_path_in_repo} via Space Builder"
239
 
240
- # Create temporary directory for cloning
241
  with tempfile.TemporaryDirectory() as temp_dir:
242
- repo_local_clone_path = Path(temp_dir) / "cloned_repo" # Use a fixed name for cloned repo dir
243
 
244
- # Clone the specific space
245
  cloned_repo = Repository(
246
- local_dir=repo_local_clone_path,
247
- clone_from=f"https://huggingface.co/spaces/{repo_id}",
248
- repo_type="space",
249
  use_auth_token=api_token,
250
- git_user="Space Builder Bot", # Optional: Git committer info
251
- git_email="space-builder@huggingface.co" # Optional
252
  )
 
253
 
254
- # Write updated file
255
- full_local_file_path = cloned_repo.local_dir / file_path_in_repo
256
  full_local_file_path.parent.mkdir(parents=True, exist_ok=True)
257
  with open(full_local_file_path, "w", encoding="utf-8") as f:
258
  f.write(file_content)
 
259
 
260
- # Commit and push changes
261
- # The Repository object automatically handles staging changes from the local_dir.
262
  cloned_repo.push_to_hub(commit_message=commit_message_ui)
 
263
 
264
  return f"Successfully updated `{file_path_in_repo}` in Space [{repo_id}](https://huggingface.co/spaces/{repo_id})"
265
 
266
  except Exception as e:
267
- logger.error(f"Error in update_space_file: {str(e)}")
268
- return f"Error updating file for `{repo_id or (owner_ui + '/' + space_name_ui if owner_ui else space_name_ui)}`: {str(e)}"
 
7
  create_repo,
8
  upload_folder,
9
  list_repo_files,
10
+ # delete_file, # Not used
11
  Repository,
12
  whoami,
13
  )
 
21
  # Function to parse markdown input
22
  def parse_markdown(markdown_input):
23
  """Parse markdown input to extract space details and file structure."""
24
+ space_info = {"repo_name_md": "", "owner_md": "", "files": []}
 
 
25
  current_file = None
26
  file_content = []
27
+ in_file_content = False # Tracks if we are inside a ### File: block content
28
+ in_code_block = False # Tracks if we are inside a ``` code block ```
29
 
30
  lines = markdown_input.strip().split("\n")
31
+ for line_idx, line_content_orig in enumerate(lines):
32
+ line_content_stripped = line_content_orig.strip()
33
+
34
+ # Handle file content collection, especially for code blocks
35
+ if in_file_content:
36
+ if line_content_stripped.startswith("```"):
37
+ if in_code_block: # Closing ```
38
+ file_content.append(line_content_orig) # Keep the closing backticks as part of content
39
+ in_code_block = False
40
+ # Don't immediately save here, let the next ### File or end of input handle it
41
+ # This allows for text after a code block but before the next file.
42
+ else: # Opening ```
43
+ in_code_block = True
44
+ file_content.append(line_content_orig)
45
+ elif in_code_block: # Inside a code block
46
+ file_content.append(line_content_orig)
47
+ elif not in_code_block: # Plain text line within ### File: block but outside ```
48
+ # Check if this line is a new file marker, if so, current file ends.
49
+ if line_content_stripped.startswith("### File:") or line_content_stripped.startswith("## File Structure") or line_content_stripped.startswith("# Space:"):
50
+ if current_file and file_content:
51
+ space_info["files"].append({"path": current_file, "content": "\n".join(file_content)})
52
+ current_file = None # Reset
53
+ file_content = []
54
+ in_file_content = False # Current file ended
55
+ # Reprocess this line if it's a new file marker (will be handled by outer ifs)
56
+ else: # Regular content line
57
+ file_content.append(line_content_orig)
58
+
59
+
60
+ # Detect major structural elements
61
+ if line_content_stripped.startswith("# Space:"):
62
+ if current_file and file_content: # Save previous file if any
63
+ space_info["files"].append({"path": current_file, "content": "\n".join(file_content)})
64
+ full_space_name_md = line_content_stripped.replace("# Space:", "").strip()
65
  if "/" in full_space_name_md:
66
  space_info["owner_md"], space_info["repo_name_md"] = full_space_name_md.split("/", 1)
67
  else:
68
  space_info["repo_name_md"] = full_space_name_md
69
+ current_file = None
70
+ file_content = []
71
+ in_file_content = False
72
+ in_code_block = False
73
+
74
+ elif line_content_stripped.startswith("## File Structure"):
75
+ if current_file and file_content: # Save previous file if any
76
  space_info["files"].append({"path": current_file, "content": "\n".join(file_content)})
77
+ current_file = None
78
+ file_content = []
79
+ in_file_content = False
80
+ in_code_block = False
81
+ continue # Just a section header
82
 
83
+ elif line_content_stripped.startswith("### File:"):
 
 
 
 
84
  if current_file and file_content: # Save content of the previous file
85
  space_info["files"].append({"path": current_file, "content": "\n".join(file_content)})
86
 
87
+ current_file = line_content_stripped.replace("### File:", "").strip()
88
  file_content = [] # Reset for new file
89
+ in_file_content = True # Start collecting content lines for this file
90
+ in_code_block = False # Reset code block state for new file
91
+
92
+ # Note: 📄 and 📁 are ignored if ### File: is the primary mechanism as implemented.
93
+ # If they are meant to define empty files, that logic would need to be added.
94
+ # Current parser prioritizes ### File: sections for content.
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  # Append the last file's content if any
97
  if current_file and file_content:
98
  space_info["files"].append({"path": current_file, "content": "\n".join(file_content)})
99
 
100
+ space_info["files"] = [f for f in space_info["files"] if f.get("path")] # Filter out empty path entries
 
101
  return space_info
102
 
103
+
104
  def _determine_repo_id(api_token, space_name_ui, owner_ui):
105
  """
106
  Determines the final owner and constructs the repo_id.
 
110
  """
111
  if not space_name_ui:
112
  return None, "Error: Space Name cannot be empty."
113
+ if "/" in space_name_ui: # User should not put slash in space name field
114
  return None, "Error: Space Name should not contain '/'. Please use the Owner field for the namespace."
115
 
116
  final_owner = owner_ui
117
  error_message = None
118
 
119
+ if not final_owner:
120
  if not api_token:
121
  return None, "Error: API token is required to automatically determine owner when Owner field is empty."
122
  try:
123
  user_info = whoami(token=api_token)
124
  if user_info and 'name' in user_info:
125
  final_owner = user_info['name']
126
+ logger.info(f"Determined owner: {final_owner} from API token.")
127
  else:
128
+ logger.error(f"whoami(token=...) returned: {user_info} - 'name' field missing or user_info is None.")
129
  error_message = "Error: Could not retrieve username from API token. Ensure token is valid and has 'Read profile' permissions. Or, specify Owner manually."
130
  except Exception as e:
131
  logger.error(f"Error calling whoami for owner: {str(e)}")
 
150
  if err:
151
  return err
152
 
 
 
153
  space_info = parse_markdown(markdown_input)
154
  if not space_info["files"]:
155
+ return "Error: No files found in the markdown input. Ensure '### File: path/to/file.ext' markers are used correctly with content."
156
 
157
  # Create temporary directory
158
  with tempfile.TemporaryDirectory() as temp_dir:
159
+ repo_local_path = Path(temp_dir) / "repo_content_for_upload"
160
+ repo_local_path.mkdir(exist_ok=True)
 
 
161
 
162
  # Write files to temporary directory
163
  for file_info in space_info["files"]:
164
  if not file_info.get("path"):
165
  logger.warning(f"Skipping file with no path: {file_info}")
166
  continue
167
+ file_path_abs = repo_local_path / file_info["path"]
168
  file_path_abs.parent.mkdir(parents=True, exist_ok=True)
169
  with open(file_path_abs, "w", encoding="utf-8") as f:
170
  f.write(file_info["content"])
 
175
  create_repo(
176
  repo_id=repo_id,
177
  token=api_token,
178
+ repo_type="space", # Correctly set
179
  space_sdk=sdk_ui,
180
+ private=False,
181
  )
182
+ logger.info(f"Created Space repo: {repo_id}")
183
  except Exception as e:
184
+ err_str = str(e).lower()
185
+ if "already exists" in err_str or "you already created this repo" in err_str or "exists" in err_str: # More robust check
186
+ logger.info(f"Space {repo_id} already exists, proceeding to upload/update files.")
187
  else:
188
  return f"Error creating Space '{repo_id}': {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  # Push to Hugging Face Space
 
191
  upload_folder(
192
  repo_id=repo_id,
193
+ folder_path=str(repo_local_path), # upload_folder expects string path
194
+ path_in_repo=".",
195
  token=api_token,
196
+ repo_type="space", # ***** ADD THIS LINE *****
197
+ commit_message=f"Initial Space setup of {repo_id} via Builder",
198
+ # allow_patterns=["*.py", "*.md", "*.txt", "Dockerfile", ".gitattributes", "*.json", "*.yaml", "*.yml"], # Example: be more specific if needed
199
+ # ignore_patterns=["*.git/*", ".*", "__pycache__/*"], # Example
200
  )
201
+ logger.info(f"Uploaded files to Space: {repo_id}")
202
  return f"Successfully created/updated Space: [{repo_id}](https://huggingface.co/spaces/{repo_id})"
203
 
204
  except Exception as e:
205
+ logger.exception(f"Error in create_space for {repo_id if 'repo_id' in locals() else 'unknown repo'}:") # Log full traceback
206
+ return f"Error during Space creation/update: {str(e)}"
207
 
208
  # Function to view Space files
209
  def view_space_files(api_token, space_name_ui, owner_ui):
210
  """List files in a Hugging Face Space."""
211
+ repo_id_for_error = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
212
  try:
213
  if not api_token:
214
  return "Error: Please provide a valid Hugging Face API token."
 
216
  repo_id, err = _determine_repo_id(api_token, space_name_ui, owner_ui)
217
  if err:
218
  return err
219
+
220
+ files = list_repo_files(repo_id=repo_id, token=api_token, repo_type="space") # Correctly set
221
  if files:
222
  return f"Files in `{repo_id}`:\n\n" + "\n".join([f"- `{f}`" for f in files])
223
  else:
224
  return f"No files found in the Space `{repo_id}`."
225
  except Exception as e:
226
+ logger.exception(f"Error in view_space_files for {repo_id_for_error}:")
227
+ return f"Error listing files for `{repo_id_for_error}`: {str(e)}"
228
 
229
  # Function to update a Space file
230
  def update_space_file(api_token, space_name_ui, owner_ui, file_path_in_repo, file_content, commit_message_ui):
231
  """Update a file in a Hugging Face Space with a commit."""
232
+ repo_id_for_error = f"{owner_ui}/{space_name_ui}" if owner_ui else space_name_ui
233
  try:
234
  if not api_token:
235
  return "Error: Please provide a valid Hugging Face API token."
 
243
  if not commit_message_ui:
244
  commit_message_ui = f"Update {file_path_in_repo} via Space Builder"
245
 
 
246
  with tempfile.TemporaryDirectory() as temp_dir:
247
+ repo_local_clone_path = Path(temp_dir) / "cloned_space_repo"
248
 
 
249
  cloned_repo = Repository(
250
+ local_dir=str(repo_local_clone_path), # Repository expects string path
251
+ clone_from=f"https://huggingface.co/spaces/{repo_id}", # Ensure this URL is correct
252
+ repo_type="space", # Correctly set
253
  use_auth_token=api_token,
254
+ git_user="Space Builder Bot",
255
+ git_email="space-builder@huggingface.co"
256
  )
257
+ logger.info(f"Cloned Space {repo_id} to {repo_local_clone_path}")
258
 
259
+ full_local_file_path = cloned_repo.local_dir / file_path_in_repo # Path object arithmetic
 
260
  full_local_file_path.parent.mkdir(parents=True, exist_ok=True)
261
  with open(full_local_file_path, "w", encoding="utf-8") as f:
262
  f.write(file_content)
263
+ logger.info(f"Wrote updated file {file_path_in_repo} locally.")
264
 
 
 
265
  cloned_repo.push_to_hub(commit_message=commit_message_ui)
266
+ logger.info(f"Pushed changes for {file_path_in_repo} to {repo_id}")
267
 
268
  return f"Successfully updated `{file_path_in_repo}` in Space [{repo_id}](https://huggingface.co/spaces/{repo_id})"
269
 
270
  except Exception as e:
271
+ logger.exception(f"Error in update_space_file for {repo_id_for_error}:")
272
+ return f"Error updating file for `{repo_id_for_error}`: {str(e)}"