akhaliq HF Staff commited on
Commit
ba8977c
Β·
1 Parent(s): b277d84

Add standalone backend_deploy.py without Gradio dependencies

Browse files

Created backend_deploy.py as a standalone deployment utility that can be
used by the backend API without any Gradio dependencies. This module
provides clean deployment functions for HuggingFace Spaces.

Key features:
- deploy_to_huggingface_space(): Main deployment function supporting all
language types (HTML, Gradio, Streamlit, React, Transformers.js, ComfyUI)
- Automatic SDK detection and appropriate file structure generation
- Multi-file support (README, Dockerfile, requirements.txt, etc.)
- update_space_file(): Update individual files in existing spaces
- delete_space(): Delete spaces
- list_user_spaces(): List all user spaces

Code parsing utilities:
- parse_html_code(): Extract HTML from various formats
- parse_transformers_js_output(): Parse multi-file transformers.js output
- parse_python_requirements(): Extract requirements.txt
- parse_multi_file_python_output(): Parse Gradio/Streamlit multi-file apps
- is_streamlit_code() / is_gradio_code(): Code type detection
- detect_sdk_from_code(): Auto-detect appropriate HF Space SDK

File generators:
- create_space_readme(): Generate README.md with proper frontmatter
- create_dockerfile_for_streamlit(): Streamlit Dockerfile
- create_dockerfile_for_react(): React Dockerfile

Updated backend_api.py:
- Simplified /api/deploy endpoint to use backend_deploy module
- Reduced code duplication and improved maintainability
- Better error handling with user-friendly messages

Updated Dockerfile:
- Added COPY for backend_deploy.py

This completes the backend modularization, making deployment logic
reusable and maintainable without heavy UI dependencies.

Files changed (3) hide show
  1. Dockerfile +1 -0
  2. backend_api.py +36 -122
  3. backend_deploy.py +501 -0
Dockerfile CHANGED
@@ -55,6 +55,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
55
  COPY --chown=user:user anycoder_app/ ./anycoder_app/
56
  COPY --chown=user:user backend_api.py .
57
  COPY --chown=user:user backend_prompts.py .
 
58
  COPY --chown=user:user app.py .
59
 
60
  # Copy built frontend from builder stage
 
55
  COPY --chown=user:user anycoder_app/ ./anycoder_app/
56
  COPY --chown=user:user backend_api.py .
57
  COPY --chown=user:user backend_prompts.py .
58
+ COPY --chown=user:user backend_deploy.py .
59
  COPY --chown=user:user app.py .
60
 
61
  # Copy built frontend from builder stage
backend_api.py CHANGED
@@ -511,19 +511,10 @@ async def deploy(
511
  # Check if this is dev mode (no real token)
512
  if auth.token and auth.token.startswith("dev_token_"):
513
  # In dev mode, open HF Spaces creation page
514
- import urllib.parse
515
  base_url = "https://huggingface.co/new-space"
516
 
517
- # Map language to SDK
518
- language_to_sdk = {
519
- "gradio": "gradio",
520
- "streamlit": "docker",
521
- "react": "docker",
522
- "html": "static",
523
- "transformers.js": "static",
524
- "comfyui": "static"
525
- }
526
- sdk = language_to_sdk.get(request.language, "gradio")
527
 
528
  params = urllib.parse.urlencode({
529
  "name": request.space_name or "my-anycoder-app",
@@ -552,9 +543,7 @@ async def deploy(
552
 
553
  # Production mode with real OAuth token
554
  try:
555
- from huggingface_hub import HfApi
556
- import tempfile
557
- import uuid
558
 
559
  # Get user token - should be the access_token from OAuth session
560
  user_token = auth.token if auth.token else os.getenv("HF_TOKEN")
@@ -564,102 +553,40 @@ async def deploy(
564
 
565
  print(f"[Deploy] Attempting deployment with token (first 10 chars): {user_token[:10]}...")
566
 
567
- # Create API client
568
- api = HfApi(token=user_token)
569
-
570
- # Get the actual username from HuggingFace API
571
- try:
572
- user_info = api.whoami()
573
- print(f"[Deploy] User info from HF API: {user_info}")
574
- username = user_info.get("name") or user_info.get("preferred_username") or auth.username or "user"
575
- except Exception as e:
576
- print(f"[Deploy] Failed to get user info from HF API: {e}")
577
- # Fallback to auth username if available
578
- username = auth.username
579
- if not username:
580
- raise HTTPException(
581
- status_code=401,
582
- detail="Failed to verify HuggingFace account. Please sign in again."
583
- )
584
-
585
- # Generate space name if not provided
586
- space_name = request.space_name or f"anycoder-{uuid.uuid4().hex[:8]}"
587
- repo_id = f"{username}/{space_name}"
588
-
589
- print(f"[Deploy] Creating/updating space: {repo_id}")
590
-
591
- # Map language to SDK
592
- language_to_sdk = {
593
- "gradio": "gradio",
594
- "streamlit": "docker",
595
- "react": "docker",
596
- "html": "static",
597
- "transformers.js": "static",
598
- "comfyui": "static"
599
- }
600
- sdk = language_to_sdk.get(request.language, "gradio")
601
-
602
- # Create the space
603
- try:
604
- api.create_repo(
605
- repo_id=repo_id,
606
- repo_type="space",
607
- space_sdk=sdk,
608
- exist_ok=False
609
- )
610
- except Exception as e:
611
- if "already exists" in str(e).lower():
612
- # Space exists, we'll update it
613
- pass
614
- else:
615
- raise
616
-
617
- # Upload the code file
618
- if request.language in ["html", "transformers.js", "comfyui"]:
619
- file_name = "index.html"
620
- else:
621
- file_name = "app.py"
622
-
623
- with tempfile.NamedTemporaryFile("w", suffix=f".{file_name.split('.')[-1]}", delete=False) as f:
624
- f.write(request.code)
625
- temp_path = f.name
626
 
627
- try:
628
- api.upload_file(
629
- path_or_fileobj=temp_path,
630
- path_in_repo=file_name,
631
- repo_id=repo_id,
632
- repo_type="space"
633
- )
634
-
635
- # For Gradio apps, also upload requirements.txt if needed
636
- if request.language == "gradio":
637
- # Simple requirements for basic Gradio app
638
- requirements = "gradio>=4.0.0\n"
639
- with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as req_f:
640
- req_f.write(requirements)
641
- req_temp_path = req_f.name
642
-
643
- try:
644
- api.upload_file(
645
- path_or_fileobj=req_temp_path,
646
- path_in_repo="requirements.txt",
647
- repo_id=repo_id,
648
- repo_type="space"
649
- )
650
- finally:
651
- os.unlink(req_temp_path)
652
-
653
- space_url = f"https://huggingface.co/spaces/{repo_id}"
654
-
655
  return {
656
  "success": True,
657
  "space_url": space_url,
658
- "message": f"βœ… Deployed successfully to {repo_id}!"
659
  }
660
-
661
- finally:
662
- os.unlink(temp_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
 
664
  except HTTPException:
665
  # Re-raise HTTP exceptions as-is
@@ -670,23 +597,10 @@ async def deploy(
670
  error_details = traceback.format_exc()
671
  print(f"[Deploy] Deployment error: {error_details}")
672
 
673
- # Provide user-friendly error message
674
- error_msg = str(e)
675
- if "401" in error_msg or "Unauthorized" in error_msg:
676
- raise HTTPException(
677
- status_code=401,
678
- detail="Authentication failed. Please sign in again with HuggingFace."
679
- )
680
- elif "403" in error_msg or "Forbidden" in error_msg:
681
- raise HTTPException(
682
- status_code=403,
683
- detail="Permission denied. Your HuggingFace token may not have the required permissions (manage-repos scope)."
684
- )
685
- else:
686
- raise HTTPException(
687
- status_code=500,
688
- detail=f"Deployment failed: {error_msg}"
689
- )
690
 
691
 
692
  @app.websocket("/ws/generate")
 
511
  # Check if this is dev mode (no real token)
512
  if auth.token and auth.token.startswith("dev_token_"):
513
  # In dev mode, open HF Spaces creation page
514
+ from backend_deploy import detect_sdk_from_code
515
  base_url = "https://huggingface.co/new-space"
516
 
517
+ sdk = detect_sdk_from_code(request.code, request.language)
 
 
 
 
 
 
 
 
 
518
 
519
  params = urllib.parse.urlencode({
520
  "name": request.space_name or "my-anycoder-app",
 
543
 
544
  # Production mode with real OAuth token
545
  try:
546
+ from backend_deploy import deploy_to_huggingface_space
 
 
547
 
548
  # Get user token - should be the access_token from OAuth session
549
  user_token = auth.token if auth.token else os.getenv("HF_TOKEN")
 
553
 
554
  print(f"[Deploy] Attempting deployment with token (first 10 chars): {user_token[:10]}...")
555
 
556
+ # Use the standalone deployment function
557
+ success, message, space_url = deploy_to_huggingface_space(
558
+ code=request.code,
559
+ language=request.language,
560
+ space_name=request.space_name,
561
+ token=user_token,
562
+ username=auth.username,
563
+ description=request.description if hasattr(request, 'description') else None,
564
+ private=False
565
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
 
567
+ if success:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  return {
569
  "success": True,
570
  "space_url": space_url,
571
+ "message": message
572
  }
573
+ else:
574
+ # Provide user-friendly error message based on the error
575
+ if "401" in message or "Unauthorized" in message:
576
+ raise HTTPException(
577
+ status_code=401,
578
+ detail="Authentication failed. Please sign in again with HuggingFace."
579
+ )
580
+ elif "403" in message or "Forbidden" in message or "Permission" in message:
581
+ raise HTTPException(
582
+ status_code=403,
583
+ detail="Permission denied. Your HuggingFace token may not have the required permissions (manage-repos scope)."
584
+ )
585
+ else:
586
+ raise HTTPException(
587
+ status_code=500,
588
+ detail=message
589
+ )
590
 
591
  except HTTPException:
592
  # Re-raise HTTP exceptions as-is
 
597
  error_details = traceback.format_exc()
598
  print(f"[Deploy] Deployment error: {error_details}")
599
 
600
+ raise HTTPException(
601
+ status_code=500,
602
+ detail=f"Deployment failed: {str(e)}"
603
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
604
 
605
 
606
  @app.websocket("/ws/generate")
backend_deploy.py ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Standalone deployment utilities for publishing to HuggingFace Spaces.
3
+ No Gradio dependencies - can be used in backend API.
4
+ """
5
+ import os
6
+ import re
7
+ import json
8
+ import uuid
9
+ import tempfile
10
+ import shutil
11
+ from typing import Dict, List, Optional, Tuple
12
+ from pathlib import Path
13
+
14
+ from huggingface_hub import HfApi
15
+
16
+
17
+ def parse_html_code(code: str) -> str:
18
+ """Extract HTML code from various formats"""
19
+ code = code.strip()
20
+
21
+ # If already clean HTML, return as-is
22
+ if code.startswith('<!DOCTYPE') or code.startswith('<html'):
23
+ return code
24
+
25
+ # Try to extract from code blocks
26
+ if '```html' in code:
27
+ match = re.search(r'```html\s*(.*?)\s*```', code, re.DOTALL)
28
+ if match:
29
+ return match.group(1).strip()
30
+
31
+ if '```' in code:
32
+ match = re.search(r'```\s*(.*?)\s*```', code, re.DOTALL)
33
+ if match:
34
+ return match.group(1).strip()
35
+
36
+ return code
37
+
38
+
39
+ def parse_transformers_js_output(code: str) -> Dict[str, str]:
40
+ """Parse transformers.js output into separate files"""
41
+ files = {}
42
+
43
+ # Pattern to match file sections
44
+ pattern = r'===\s*(\S+\.(?:html|js|css))\s*===\s*(.*?)(?====|$)'
45
+ matches = re.finditer(pattern, code, re.DOTALL | re.IGNORECASE)
46
+
47
+ for match in matches:
48
+ filename = match.group(1).strip()
49
+ content = match.group(2).strip()
50
+
51
+ # Clean up code blocks if present
52
+ content = re.sub(r'^```\w*\s*', '', content, flags=re.MULTILINE)
53
+ content = re.sub(r'```\s*$', '', content, flags=re.MULTILINE)
54
+
55
+ files[filename] = content
56
+
57
+ # If no files were parsed, try to extract as single HTML file
58
+ if not files:
59
+ html_content = parse_html_code(code)
60
+ if html_content:
61
+ files['index.html'] = html_content
62
+
63
+ return files
64
+
65
+
66
+ def parse_python_requirements(code: str) -> Optional[str]:
67
+ """Extract requirements.txt content from code if present"""
68
+ # Look for requirements.txt section
69
+ req_pattern = r'===\s*requirements\.txt\s*===\s*(.*?)(?====|$)'
70
+ match = re.search(req_pattern, code, re.DOTALL | re.IGNORECASE)
71
+
72
+ if match:
73
+ requirements = match.group(1).strip()
74
+ # Clean up code blocks
75
+ requirements = re.sub(r'^```\w*\s*', '', requirements, flags=re.MULTILINE)
76
+ requirements = re.sub(r'```\s*$', '', requirements, flags=re.MULTILINE)
77
+ return requirements
78
+
79
+ return None
80
+
81
+
82
+ def parse_multi_file_python_output(code: str) -> Dict[str, str]:
83
+ """Parse multi-file Python output (e.g., Gradio, Streamlit)"""
84
+ files = {}
85
+
86
+ # Pattern to match file sections
87
+ pattern = r'===\s*(\S+\.(?:py|txt))\s*===\s*(.*?)(?====|$)'
88
+ matches = re.finditer(pattern, code, re.DOTALL | re.IGNORECASE)
89
+
90
+ for match in matches:
91
+ filename = match.group(1).strip()
92
+ content = match.group(2).strip()
93
+
94
+ # Clean up code blocks if present
95
+ content = re.sub(r'^```\w*\s*', '', content, flags=re.MULTILINE)
96
+ content = re.sub(r'```\s*$', '', content, flags=re.MULTILINE)
97
+
98
+ files[filename] = content
99
+
100
+ # If no files were parsed, treat as single app.py
101
+ if not files:
102
+ # Clean up code blocks
103
+ clean_code = re.sub(r'^```\w*\s*', '', code, flags=re.MULTILINE)
104
+ clean_code = re.sub(r'```\s*$', '', clean_code, flags=re.MULTILINE)
105
+ files['app.py'] = clean_code.strip()
106
+
107
+ return files
108
+
109
+
110
+ def is_streamlit_code(code: str) -> bool:
111
+ """Check if code is Streamlit"""
112
+ return 'import streamlit' in code or 'streamlit.run' in code
113
+
114
+
115
+ def is_gradio_code(code: str) -> bool:
116
+ """Check if code is Gradio"""
117
+ return 'import gradio' in code or 'gr.' in code
118
+
119
+
120
+ def detect_sdk_from_code(code: str, language: str) -> str:
121
+ """Detect the appropriate SDK from code and language"""
122
+ if language == "html":
123
+ return "static"
124
+ elif language == "transformers.js":
125
+ return "static"
126
+ elif language == "comfyui":
127
+ return "static"
128
+ elif language == "react":
129
+ return "docker"
130
+ elif language == "streamlit" or is_streamlit_code(code):
131
+ return "docker"
132
+ elif language == "gradio" or is_gradio_code(code):
133
+ return "gradio"
134
+ else:
135
+ return "gradio" # Default
136
+
137
+
138
+ def create_space_readme(space_name: str, sdk: str, description: str = None) -> str:
139
+ """Create README.md for HuggingFace Space"""
140
+ readme = f"""---
141
+ title: {space_name}
142
+ emoji: πŸš€
143
+ colorFrom: blue
144
+ colorTo: purple
145
+ sdk: {sdk}
146
+ sdk_version: 4.44.1
147
+ app_file: app.py
148
+ pinned: false
149
+ """
150
+
151
+ if sdk == "docker":
152
+ readme += "app_port: 7860\n"
153
+
154
+ readme += "---\n\n"
155
+
156
+ if description:
157
+ readme += f"{description}\n\n"
158
+ else:
159
+ readme += f"# {space_name}\n\nBuilt with [anycoder](https://huggingface.co/spaces/akhaliq/anycoder)\n"
160
+
161
+ return readme
162
+
163
+
164
+ def create_dockerfile_for_streamlit(space_name: str) -> str:
165
+ """Create Dockerfile for Streamlit app"""
166
+ return f"""FROM python:3.11-slim
167
+
168
+ WORKDIR /app
169
+
170
+ COPY requirements.txt .
171
+ RUN pip install --no-cache-dir -r requirements.txt
172
+
173
+ COPY . .
174
+
175
+ EXPOSE 7860
176
+
177
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
178
+ """
179
+
180
+
181
+ def create_dockerfile_for_react(space_name: str) -> str:
182
+ """Create Dockerfile for React app"""
183
+ return f"""FROM node:18-slim
184
+
185
+ # Use existing node user
186
+ USER node
187
+ ENV HOME=/home/node
188
+ ENV PATH=/home/node/.local/bin:$PATH
189
+
190
+ WORKDIR /home/node/app
191
+
192
+ COPY --chown=node:node package*.json ./
193
+ RUN npm install
194
+
195
+ COPY --chown=node:node . .
196
+ RUN npm run build
197
+
198
+ EXPOSE 7860
199
+
200
+ CMD ["npm", "start", "--", "-p", "7860"]
201
+ """
202
+
203
+
204
+ def deploy_to_huggingface_space(
205
+ code: str,
206
+ language: str,
207
+ space_name: Optional[str] = None,
208
+ token: Optional[str] = None,
209
+ username: Optional[str] = None,
210
+ description: Optional[str] = None,
211
+ private: bool = False
212
+ ) -> Tuple[bool, str, Optional[str]]:
213
+ """
214
+ Deploy code to HuggingFace Spaces
215
+
216
+ Args:
217
+ code: Generated code to deploy
218
+ language: Target language/framework (html, gradio, streamlit, react, transformers.js, comfyui)
219
+ space_name: Name for the space (auto-generated if None)
220
+ token: HuggingFace API token
221
+ username: HuggingFace username
222
+ description: Space description
223
+ private: Whether to make the space private
224
+
225
+ Returns:
226
+ Tuple of (success: bool, message: str, space_url: Optional[str])
227
+ """
228
+ if not token:
229
+ token = os.getenv("HF_TOKEN")
230
+ if not token:
231
+ return False, "No HuggingFace token provided", None
232
+
233
+ try:
234
+ api = HfApi(token=token)
235
+
236
+ # Get username if not provided
237
+ if not username:
238
+ try:
239
+ user_info = api.whoami()
240
+ username = user_info.get("name") or user_info.get("preferred_username") or "user"
241
+ except Exception as e:
242
+ return False, f"Failed to get user info: {str(e)}", None
243
+
244
+ # Generate space name if not provided
245
+ if not space_name:
246
+ space_name = f"anycoder-{uuid.uuid4().hex[:8]}"
247
+
248
+ # Clean space name (no spaces, lowercase, alphanumeric + hyphens)
249
+ space_name = re.sub(r'[^a-z0-9-]', '-', space_name.lower())
250
+ space_name = re.sub(r'-+', '-', space_name).strip('-')
251
+
252
+ repo_id = f"{username}/{space_name}"
253
+
254
+ # Detect SDK
255
+ sdk = detect_sdk_from_code(code, language)
256
+
257
+ # Create temporary directory for files
258
+ with tempfile.TemporaryDirectory() as temp_dir:
259
+ temp_path = Path(temp_dir)
260
+
261
+ # Parse code based on language
262
+ if language == "transformers.js":
263
+ files = parse_transformers_js_output(code)
264
+
265
+ # Write transformers.js files
266
+ for filename, content in files.items():
267
+ (temp_path / filename).write_text(content, encoding='utf-8')
268
+
269
+ # Create README
270
+ readme = create_space_readme(space_name, "static", description)
271
+ (temp_path / "README.md").write_text(readme, encoding='utf-8')
272
+
273
+ elif language == "html":
274
+ html_code = parse_html_code(code)
275
+ (temp_path / "index.html").write_text(html_code, encoding='utf-8')
276
+
277
+ # Create README
278
+ readme = create_space_readme(space_name, "static", description)
279
+ (temp_path / "README.md").write_text(readme, encoding='utf-8')
280
+
281
+ elif language == "comfyui":
282
+ # ComfyUI is JSON, wrap in HTML viewer
283
+ (temp_path / "index.html").write_text(code, encoding='utf-8')
284
+
285
+ # Create README
286
+ readme = create_space_readme(space_name, "static", description)
287
+ (temp_path / "README.md").write_text(readme, encoding='utf-8')
288
+
289
+ elif language in ["gradio", "streamlit"]:
290
+ files = parse_multi_file_python_output(code)
291
+
292
+ # Write Python files
293
+ for filename, content in files.items():
294
+ (temp_path / filename).write_text(content, encoding='utf-8')
295
+
296
+ # Ensure requirements.txt exists
297
+ if "requirements.txt" not in files:
298
+ if language == "gradio":
299
+ (temp_path / "requirements.txt").write_text("gradio>=4.0.0\n", encoding='utf-8')
300
+ elif language == "streamlit":
301
+ (temp_path / "requirements.txt").write_text("streamlit>=1.30.0\n", encoding='utf-8')
302
+
303
+ # Create Dockerfile if needed
304
+ if sdk == "docker":
305
+ if language == "streamlit":
306
+ dockerfile = create_dockerfile_for_streamlit(space_name)
307
+ (temp_path / "Dockerfile").write_text(dockerfile, encoding='utf-8')
308
+
309
+ # Create README
310
+ readme = create_space_readme(space_name, sdk, description)
311
+ (temp_path / "README.md").write_text(readme, encoding='utf-8')
312
+
313
+ elif language == "react":
314
+ # For React, we'd need package.json and other files
315
+ # This is more complex, so for now just create a placeholder
316
+ files = parse_multi_file_python_output(code)
317
+
318
+ for filename, content in files.items():
319
+ (temp_path / filename).write_text(content, encoding='utf-8')
320
+
321
+ # Create Dockerfile
322
+ dockerfile = create_dockerfile_for_react(space_name)
323
+ (temp_path / "Dockerfile").write_text(dockerfile, encoding='utf-8')
324
+
325
+ # Create README
326
+ readme = create_space_readme(space_name, "docker", description)
327
+ (temp_path / "README.md").write_text(readme, encoding='utf-8')
328
+
329
+ else:
330
+ # Default: treat as Gradio app
331
+ files = parse_multi_file_python_output(code)
332
+
333
+ for filename, content in files.items():
334
+ (temp_path / filename).write_text(content, encoding='utf-8')
335
+
336
+ if "requirements.txt" not in files:
337
+ (temp_path / "requirements.txt").write_text("gradio>=4.0.0\n", encoding='utf-8')
338
+
339
+ # Create README
340
+ readme = create_space_readme(space_name, "gradio", description)
341
+ (temp_path / "README.md").write_text(readme, encoding='utf-8')
342
+
343
+ # Create the space
344
+ try:
345
+ api.create_repo(
346
+ repo_id=repo_id,
347
+ repo_type="space",
348
+ space_sdk=sdk,
349
+ private=private,
350
+ exist_ok=False
351
+ )
352
+ except Exception as e:
353
+ if "already exists" in str(e).lower():
354
+ # Space exists, we'll update it
355
+ pass
356
+ else:
357
+ return False, f"Failed to create space: {str(e)}", None
358
+
359
+ # Upload all files
360
+ try:
361
+ api.upload_folder(
362
+ folder_path=str(temp_path),
363
+ repo_id=repo_id,
364
+ repo_type="space",
365
+ commit_message="Deploy from anycoder"
366
+ )
367
+ except Exception as e:
368
+ return False, f"Failed to upload files: {str(e)}", None
369
+
370
+ space_url = f"https://huggingface.co/spaces/{repo_id}"
371
+ return True, f"βœ… Successfully deployed to {repo_id}!", space_url
372
+
373
+ except Exception as e:
374
+ return False, f"Deployment error: {str(e)}", None
375
+
376
+
377
+ def update_space_file(
378
+ repo_id: str,
379
+ file_path: str,
380
+ content: str,
381
+ token: Optional[str] = None,
382
+ commit_message: Optional[str] = None
383
+ ) -> Tuple[bool, str]:
384
+ """
385
+ Update a single file in an existing HuggingFace Space
386
+
387
+ Args:
388
+ repo_id: Full repo ID (username/space-name)
389
+ file_path: Path of file to update (e.g., "app.py")
390
+ content: New file content
391
+ token: HuggingFace API token
392
+ commit_message: Commit message (default: "Update {file_path}")
393
+
394
+ Returns:
395
+ Tuple of (success: bool, message: str)
396
+ """
397
+ if not token:
398
+ token = os.getenv("HF_TOKEN")
399
+ if not token:
400
+ return False, "No HuggingFace token provided"
401
+
402
+ try:
403
+ api = HfApi(token=token)
404
+
405
+ if not commit_message:
406
+ commit_message = f"Update {file_path}"
407
+
408
+ # Create temporary file
409
+ with tempfile.NamedTemporaryFile(mode='w', suffix=f'.{file_path.split(".")[-1]}', delete=False) as f:
410
+ f.write(content)
411
+ temp_path = f.name
412
+
413
+ try:
414
+ api.upload_file(
415
+ path_or_fileobj=temp_path,
416
+ path_in_repo=file_path,
417
+ repo_id=repo_id,
418
+ repo_type="space",
419
+ commit_message=commit_message
420
+ )
421
+ return True, f"βœ… Successfully updated {file_path}"
422
+ finally:
423
+ os.unlink(temp_path)
424
+
425
+ except Exception as e:
426
+ return False, f"Failed to update file: {str(e)}"
427
+
428
+
429
+ def delete_space(
430
+ repo_id: str,
431
+ token: Optional[str] = None
432
+ ) -> Tuple[bool, str]:
433
+ """
434
+ Delete a HuggingFace Space
435
+
436
+ Args:
437
+ repo_id: Full repo ID (username/space-name)
438
+ token: HuggingFace API token
439
+
440
+ Returns:
441
+ Tuple of (success: bool, message: str)
442
+ """
443
+ if not token:
444
+ token = os.getenv("HF_TOKEN")
445
+ if not token:
446
+ return False, "No HuggingFace token provided"
447
+
448
+ try:
449
+ api = HfApi(token=token)
450
+ api.delete_repo(repo_id=repo_id, repo_type="space")
451
+ return True, f"βœ… Successfully deleted {repo_id}"
452
+ except Exception as e:
453
+ return False, f"Failed to delete space: {str(e)}"
454
+
455
+
456
+ def list_user_spaces(
457
+ username: Optional[str] = None,
458
+ token: Optional[str] = None
459
+ ) -> Tuple[bool, str, Optional[List[Dict]]]:
460
+ """
461
+ List all spaces for a user
462
+
463
+ Args:
464
+ username: HuggingFace username (gets from token if None)
465
+ token: HuggingFace API token
466
+
467
+ Returns:
468
+ Tuple of (success: bool, message: str, spaces: Optional[List[Dict]])
469
+ """
470
+ if not token:
471
+ token = os.getenv("HF_TOKEN")
472
+ if not token:
473
+ return False, "No HuggingFace token provided", None
474
+
475
+ try:
476
+ api = HfApi(token=token)
477
+
478
+ # Get username if not provided
479
+ if not username:
480
+ user_info = api.whoami()
481
+ username = user_info.get("name") or user_info.get("preferred_username")
482
+
483
+ # List spaces
484
+ spaces = api.list_spaces(author=username)
485
+
486
+ space_list = []
487
+ for space in spaces:
488
+ space_list.append({
489
+ "id": space.id,
490
+ "author": space.author,
491
+ "name": getattr(space, 'name', space.id.split('/')[-1]),
492
+ "sdk": getattr(space, 'sdk', 'unknown'),
493
+ "private": getattr(space, 'private', False),
494
+ "url": f"https://huggingface.co/spaces/{space.id}"
495
+ })
496
+
497
+ return True, f"Found {len(space_list)} spaces", space_list
498
+
499
+ except Exception as e:
500
+ return False, f"Failed to list spaces: {str(e)}", None
501
+