Spaces:
Running
Running
File size: 13,460 Bytes
1e5eb40 1579e31 1e5eb40 1579e31 b257fdf 1579e31 90bdafc 1579e31 90bdafc 1579e31 90bdafc 1579e31 90bdafc 1579e31 90bdafc e9dd265 8cc42bf 90bdafc 1579e31 1890cbb b257fdf 1e5eb40 e2897a1 c08b935 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
from flask import Flask, request, jsonify, Response
import os
import json
from huggingface_hub import HfApi, create_repo, upload_file
import random
import string
app = Flask(__name__)
# Hugging Face API token (set in Space settings)
HF_TOKEN = os.getenv("HF_TOKEN")
if not HF_TOKEN:
raise ValueError("HF_TOKEN not set. Add it in Space settings.")
hf_api = HfApi()
# Documentation as a string
DOCUMENTATION = """# Assembler Space API Documentation
## Overview
The **Assembler Space** is a Hugging Face Space that acts as a factory for creating new Hugging Face Spaces dynamically. It exposes a Flask-based API that accepts JSON input containing code, files, parameters, and configuration details. Upon receiving a valid request, it creates a new Space repository on Hugging Face, populates it with the provided files, and triggers its deployment. The API supports multiple Space types (e.g., `gradio`, `static`, `docker`, `streamlit`) and multi-file submissions, making it versatile for generating a wide range of applications.
The Assembler Space itself runs as a Docker-based Space on Hugging Face, accessible via a public URL: `https://broadfield-dev-assembler.hf.space`.
## Base URL
The API is hosted at:
https://broadfield-dev-assembler.hf.space/create-space
## Endpoints
### `POST /create-space`
Creates a new Hugging Face Space based on the provided JSON payload.
#### Request Format
- **Method**: `POST`
- **Content-Type**: `application/json`
- **Body**: A JSON object with the following fields:
| Field | Type | Required | Description |
|--------------|----------|----------|-----------------------------------------------------------------------------|
| `space_type` | String | No | The type of Space to create. Options: `gradio`, `static`, `docker`, `streamlit`. Defaults to `gradio` if omitted. |
| `files` | Object | Yes | A dictionary where keys are filenames (e.g., `app.py`, `index.html`) and values are file contents as strings. |
| `parameters` | Object | No | A dictionary of key-value pairs to be injected into Python files as `PARAMS` or used by the generated Space’s code. |
#### Request Constraints
- At least one file must be provided in `files`.
- Filenames in `files` should include extensions (e.g., `.py`, `.html`, `.css`, `.txt`) to ensure correct handling.
- `space_type` must match one of the supported values, or the request will fail.
- File contents should be valid for the intended `space_type` (e.g., Python code for `gradio` or `docker`, HTML for `static`).
#### Response Format
- **Content-Type**: `application/json`
- **Status Codes**:
- `200 OK`: Space creation succeeded.
- `400 Bad Request`: Invalid JSON or missing/invalid fields.
- `500 Internal Server Error`: Unexpected error during Space creation.
- **Body**: A JSON object with the following fields:
| Field | Type | Description |
|-----------|--------|-----------------------------------------------------------------------------|
| `message` | String | A brief status message (e.g., `"New Space created"`). |
| `url` | String | The URL of the newly created Space (e.g., `https://huggingface.co/spaces/<username>/<space-name>`). |
| `note` | String | Additional information (e.g., deployment time warning). |
| `error` | String | (Only in error responses) Description of what went wrong. |
### `GET /docs`
Returns this documentation as plain text in Markdown format.
#### Request Format
- **Method**: `GET`
- **Content-Type**: None required
#### Response Format
- **Content-Type**: `text/plain`
- **Status Codes**:
- `200 OK`: Documentation returned successfully.
- **Body**: The full Markdown documentation as a string.
#### Example Requests and Responses
##### Example 1: Static Space
**Request (`POST /create-space`):**
~~~json
{
"space_type": "static",
"files": {
"index.html": "<html><body><h1>Hello World</h1><p>Message: {{params['message']}}</p></body></html>",
"style.css": "h1 { color: green; }"
},
"parameters": {
"message": "Static Space Test"
}
}
~~~
**Response (200 OK):**
~~~json
{
"message": "New Space created",
"url": "https://huggingface.co/spaces/broadfield-dev/GeneratedSpace-abc123",
"note": "It may take a few minutes to build and deploy."
}
~~~
**Notes:**
- Static Spaces don’t automatically process `parameters`. The new Space’s code must handle them (e.g., via JavaScript or server-side templating if added).
##### Example 2: Docker Space with Flask
**Request (`POST /create-space`):**
~~~json
{
"space_type": "docker",
"files": {
"app.py": "from flask import Flask\\napp = Flask(__name__)\\[email protected]('/')\\ndef home():\\n return f'Hello, {PARAMS['name']}'\\nif __name__ == '__main__':\\n app.run(host='0.0.0.0', port=7860)",
"requirements.txt": "flask",
"Dockerfile": "FROM python:3.10-slim\\nWORKDIR /app\\nCOPY . .\\nRUN pip install -r requirements.txt\\nEXPOSE 7860\\nCMD [\\"python\\", \\"app.py\\"]"
},
"parameters": {
"name": "Docker User"
}
}
~~~
**Response (200 OK):**
~~~json
{
"message": "New Space created",
"url": "https://huggingface.co/spaces/broadfield-dev/GeneratedSpace-xyz789",
"note": "It may take a few minutes to build and deploy."
}
~~~
**Notes:**
- The `parameters` are injected into `app.py` as `PARAMS`. The port is set to 7860 to match Hugging Face’s default.
##### Example 3: Gradio Space
**Request (`POST /create-space`):**
~~~json
{
"space_type": "gradio",
"files": {
"app.py": "import gradio as gr\\ndef greet():\\n return f'Hi, {PARAMS['user']}'\\ninterface = gr.Interface(fn=greet, inputs=None, outputs='text')\\ninterface.launch()"
},
"parameters": {
"user": "Gradio Fan"
}
}
~~~
**Response (200 OK):**
~~~json
{
"message": "New Space created",
"url": "https://huggingface.co/spaces/broadfield-dev/GeneratedSpace-def456",
"note": "It may take a few minutes to build and deploy."
}
~~~
##### Example 4: Invalid Request
**Request (`POST /create-space`):**
~~~json
{
"space_type": "invalid",
"files": {}
}
~~~
**Response (400 Bad Request):**
~~~json
{
"error": "Invalid space_type. Must be one of ['gradio', 'static', 'docker', 'streamlit']"
}
~~~
##### Example 5: Get Documentation
**Request (`GET /docs`):**
GET https://broadfield-dev-assembler.hf.space/docs
**Response (200 OK):**
<The full Markdown text of this documentation>
```
Usage Example (Python)
Here’s how to call the API using Python’s requests library:
python
import requests
# Create a new Space
url = "https://broadfield-dev-assembler.hf.space/create-space"
payload = {
"space_type": "static",
"files": {
"index.html": "<html><body><h1>Test Page</h1></body></html>"
},
"parameters": {
"key": "value"
}
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload)
if response.status_code == 200:
print("Success:", response.json())
else:
print("Error:", response.status_code, response.json())
# Fetch documentation
docs_url = "https://broadfield-dev-assembler.hf.space/docs"
docs_response = requests.get(docs_url)
if docs_response.status_code == 200:
print("Documentation:", docs_response.text)
Additional Details
Supported Space Types
gradio: For interactive Python apps using the Gradio framework. Requires an app.py with Gradio code.
static: For static websites. Requires at least an index.html file; supports additional files like style.css.
docker: For custom apps (e.g., Flask, FastAPI). Requires a Dockerfile or uses a default one if omitted.
streamlit: For Streamlit apps. Requires an app.py with Streamlit code.
File Handling
Python Files (.py): The parameters object is injected as a global PARAMS variable at the top of the file.
Other Files: Contents are uploaded as-is; no automatic parameter injection (e.g., HTML files need custom logic to use parameters).
Requirements: If requirements.txt isn’t provided, a default is generated based on space_type (e.g., gradio for Gradio, flask for Docker).
Deployment Notes
Build Time: New Spaces take 1-5 minutes to build and deploy on Hugging Face. The API returns immediately with a URL, but the Space won’t be live until the build completes.
Port for Docker: Hugging Face expects Docker Spaces to listen on port 7860. Ensure your Dockerfile and app code align with this (see Example 2).
Security Considerations
The Assembler doesn’t execute the provided code; it only creates a new Space. However, ensure the generated Space’s code is safe if deploying publicly.
Avoid sending sensitive data in parameters unless the new Space is private (not implemented here but can be adjusted).
Limitations
Rate Limits: Hugging Face may restrict frequent Space creation. Check their API documentation for quotas.
File Size: Large files may fail to upload; keep contents reasonable (e.g., <1MB per file).
Error Handling: Basic validation is included, but complex syntax checking isn’t performed.
"""
def generate_space_name():
"""Generate a unique Space name."""
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
return f"GeneratedSpace-{random_suffix}"
@app.route('/create-space', methods=['POST'])
def create_hf_space():
try:
# Parse JSON input
data = request.get_json()
if not data:
return jsonify({"error": "No JSON data provided"}), 400
# Extract parameters
space_type = data.get("space_type", "gradio") # Default to gradio if not specified
files = data.get("files", {}) # Dictionary of filename: content
params = data.get("parameters", {}) # Optional parameters
if not files:
return jsonify({"error": "No files provided in JSON"}), 400
# Validate space_type
valid_space_types = ["gradio", "static", "docker", "streamlit"]
if space_type not in valid_space_types:
return jsonify({"error": f"Invalid space_type. Must be one of {valid_space_types}"}), 400
# Create a unique Space name and repo
space_name = generate_space_name()
full_repo_id = f"{hf_api.whoami(HF_TOKEN)['name']}/{space_name}"
create_repo(
repo_id=space_name,
repo_type="space",
space_sdk=space_type,
token=HF_TOKEN,
private=False
)
# Handle multi-file uploads
for filename, content in files.items():
# Write content to a temporary file
with open(f"temp_{filename}", "w") as f:
if filename.endswith(".py"):
# Inject parameters into Python files if present
content = f"PARAMS = {json.dumps(params)}\n\n{content}"
f.write(content)
# Upload to the new Space
upload_file(
path_or_fileobj=f"temp_{filename}",
path_in_repo=filename,
repo_id=full_repo_id,
repo_type="space",
token=HF_TOKEN
)
os.remove(f"temp_{filename}")
# Add requirements.txt if not provided (basic defaults)
if "requirements.txt" not in files:
default_requirements = {
"gradio": "gradio",
"static": "",
"docker": "flask", # Example; adjust based on needs
"streamlit": "streamlit"
}.get(space_type, "")
with open("temp_requirements.txt", "w") as f:
f.write(default_requirements)
upload_file(
path_or_fileobj="temp_requirements.txt",
path_in_repo="requirements.txt",
repo_id=full_repo_id,
repo_type="space",
token=HF_TOKEN
)
os.remove("temp_requirements.txt")
# Special handling for Docker Spaces
if space_type == "docker" and "Dockerfile" not in files:
default_dockerfile = """
FROM python:3.10-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
EXPOSE 7860
CMD ["python", "app.py"]
"""
with open("temp_Dockerfile", "w") as f:
f.write(default_dockerfile)
upload_file(
path_or_fileobj="temp_Dockerfile",
path_in_repo="Dockerfile",
repo_id=full_repo_id,
repo_type="space",
token=HF_TOKEN
)
os.remove("temp_Dockerfile")
space_url = f"https://huggingface.co/spaces/{full_repo_id}"
return jsonify({
"message": "New Space created",
"url": space_url,
"note": "It may take a few minutes to build and deploy."
}), 200
except json.JSONDecodeError:
return jsonify({"error": "Invalid JSON format"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/docs', methods=['GET'])
def get_docs():
"""Return the API documentation as plain text."""
return Response(DOCUMENTATION, mimetype='text/plain'), 200
if '__name__'== '__main__':
app.run(host='0.0.0.0', port=7860) |