broadfield-dev commited on
Commit
1e5eb40
·
verified ·
1 Parent(s): 8460194

Update app.py

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