createsonline 0.1.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. createsonline/__init__.py +46 -0
  2. createsonline/admin/__init__.py +7 -0
  3. createsonline/admin/content.py +526 -0
  4. createsonline/admin/crud.py +805 -0
  5. createsonline/admin/field_builder.py +559 -0
  6. createsonline/admin/integration.py +482 -0
  7. createsonline/admin/interface.py +2562 -0
  8. createsonline/admin/model_creator.py +513 -0
  9. createsonline/admin/model_manager.py +388 -0
  10. createsonline/admin/modern_dashboard.py +498 -0
  11. createsonline/admin/permissions.py +264 -0
  12. createsonline/admin/user_forms.py +594 -0
  13. createsonline/ai/__init__.py +202 -0
  14. createsonline/ai/fields.py +1226 -0
  15. createsonline/ai/orm.py +325 -0
  16. createsonline/ai/services.py +1244 -0
  17. createsonline/app.py +506 -0
  18. createsonline/auth/__init__.py +8 -0
  19. createsonline/auth/management.py +228 -0
  20. createsonline/auth/models.py +552 -0
  21. createsonline/cli/__init__.py +5 -0
  22. createsonline/cli/commands/__init__.py +122 -0
  23. createsonline/cli/commands/database.py +416 -0
  24. createsonline/cli/commands/info.py +173 -0
  25. createsonline/cli/commands/initdb.py +218 -0
  26. createsonline/cli/commands/project.py +545 -0
  27. createsonline/cli/commands/serve.py +173 -0
  28. createsonline/cli/commands/shell.py +93 -0
  29. createsonline/cli/commands/users.py +148 -0
  30. createsonline/cli/main.py +2041 -0
  31. createsonline/cli/manage.py +274 -0
  32. createsonline/config/__init__.py +9 -0
  33. createsonline/config/app.py +2577 -0
  34. createsonline/config/database.py +179 -0
  35. createsonline/config/docs.py +384 -0
  36. createsonline/config/errors.py +160 -0
  37. createsonline/config/orm.py +43 -0
  38. createsonline/config/request.py +93 -0
  39. createsonline/config/settings.py +176 -0
  40. createsonline/data/__init__.py +23 -0
  41. createsonline/data/dataframe.py +925 -0
  42. createsonline/data/io.py +453 -0
  43. createsonline/data/series.py +557 -0
  44. createsonline/database/__init__.py +60 -0
  45. createsonline/database/abstraction.py +440 -0
  46. createsonline/database/assistant.py +585 -0
  47. createsonline/database/fields.py +442 -0
  48. createsonline/database/migrations.py +132 -0
  49. createsonline/database/models.py +604 -0
  50. createsonline/database.py +438 -0
  51. createsonline/http/__init__.py +28 -0
  52. createsonline/http/client.py +535 -0
  53. createsonline/ml/__init__.py +55 -0
  54. createsonline/ml/classification.py +552 -0
  55. createsonline/ml/clustering.py +680 -0
  56. createsonline/ml/metrics.py +542 -0
  57. createsonline/ml/neural.py +560 -0
  58. createsonline/ml/preprocessing.py +784 -0
  59. createsonline/ml/regression.py +501 -0
  60. createsonline/performance/__init__.py +19 -0
  61. createsonline/performance/cache.py +444 -0
  62. createsonline/performance/compression.py +335 -0
  63. createsonline/performance/core.py +419 -0
  64. createsonline/project_init.py +789 -0
  65. createsonline/routing.py +528 -0
  66. createsonline/security/__init__.py +34 -0
  67. createsonline/security/core.py +811 -0
  68. createsonline/security/encryption.py +349 -0
  69. createsonline/server.py +295 -0
  70. createsonline/static/css/admin.css +263 -0
  71. createsonline/static/css/common.css +358 -0
  72. createsonline/static/css/dashboard.css +89 -0
  73. createsonline/static/favicon.ico +0 -0
  74. createsonline/static/icons/icon-128x128.png +0 -0
  75. createsonline/static/icons/icon-128x128.webp +0 -0
  76. createsonline/static/icons/icon-16x16.png +0 -0
  77. createsonline/static/icons/icon-16x16.webp +0 -0
  78. createsonline/static/icons/icon-180x180.png +0 -0
  79. createsonline/static/icons/icon-180x180.webp +0 -0
  80. createsonline/static/icons/icon-192x192.png +0 -0
  81. createsonline/static/icons/icon-192x192.webp +0 -0
  82. createsonline/static/icons/icon-256x256.png +0 -0
  83. createsonline/static/icons/icon-256x256.webp +0 -0
  84. createsonline/static/icons/icon-32x32.png +0 -0
  85. createsonline/static/icons/icon-32x32.webp +0 -0
  86. createsonline/static/icons/icon-384x384.png +0 -0
  87. createsonline/static/icons/icon-384x384.webp +0 -0
  88. createsonline/static/icons/icon-48x48.png +0 -0
  89. createsonline/static/icons/icon-48x48.webp +0 -0
  90. createsonline/static/icons/icon-512x512.png +0 -0
  91. createsonline/static/icons/icon-512x512.webp +0 -0
  92. createsonline/static/icons/icon-64x64.png +0 -0
  93. createsonline/static/icons/icon-64x64.webp +0 -0
  94. createsonline/static/image/android-chrome-192x192.png +0 -0
  95. createsonline/static/image/android-chrome-512x512.png +0 -0
  96. createsonline/static/image/apple-touch-icon.png +0 -0
  97. createsonline/static/image/favicon-16x16.png +0 -0
  98. createsonline/static/image/favicon-32x32.png +0 -0
  99. createsonline/static/image/favicon.ico +0 -0
  100. createsonline/static/image/favicon.svg +17 -0
  101. createsonline/static/image/icon-128x128.png +0 -0
  102. createsonline/static/image/icon-128x128.webp +0 -0
  103. createsonline/static/image/icon-16x16.png +0 -0
  104. createsonline/static/image/icon-16x16.webp +0 -0
  105. createsonline/static/image/icon-180x180.png +0 -0
  106. createsonline/static/image/icon-180x180.webp +0 -0
  107. createsonline/static/image/icon-192x192.png +0 -0
  108. createsonline/static/image/icon-192x192.webp +0 -0
  109. createsonline/static/image/icon-256x256.png +0 -0
  110. createsonline/static/image/icon-256x256.webp +0 -0
  111. createsonline/static/image/icon-32x32.png +0 -0
  112. createsonline/static/image/icon-32x32.webp +0 -0
  113. createsonline/static/image/icon-384x384.png +0 -0
  114. createsonline/static/image/icon-384x384.webp +0 -0
  115. createsonline/static/image/icon-48x48.png +0 -0
  116. createsonline/static/image/icon-48x48.webp +0 -0
  117. createsonline/static/image/icon-512x512.png +0 -0
  118. createsonline/static/image/icon-512x512.webp +0 -0
  119. createsonline/static/image/icon-64x64.png +0 -0
  120. createsonline/static/image/icon-64x64.webp +0 -0
  121. createsonline/static/image/logo-header-h100.png +0 -0
  122. createsonline/static/image/logo-header-h100.webp +0 -0
  123. createsonline/static/image/logo-header-h200@2x.png +0 -0
  124. createsonline/static/image/logo-header-h200@2x.webp +0 -0
  125. createsonline/static/image/logo.png +0 -0
  126. createsonline/static/js/admin.js +274 -0
  127. createsonline/static/site.webmanifest +35 -0
  128. createsonline/static/templates/admin/base.html +87 -0
  129. createsonline/static/templates/admin/dashboard.html +217 -0
  130. createsonline/static/templates/admin/model_form.html +270 -0
  131. createsonline/static/templates/admin/model_list.html +202 -0
  132. createsonline/static/test_script.js +15 -0
  133. createsonline/static/test_styles.css +59 -0
  134. createsonline/static_files.py +365 -0
  135. createsonline/templates/404.html +100 -0
  136. createsonline/templates/admin_login.html +169 -0
  137. createsonline/templates/base.html +102 -0
  138. createsonline/templates/index.html +151 -0
  139. createsonline/templates.py +205 -0
  140. createsonline/testing.py +322 -0
  141. createsonline/utils.py +448 -0
  142. createsonline/validation/__init__.py +49 -0
  143. createsonline/validation/fields.py +598 -0
  144. createsonline/validation/models.py +504 -0
  145. createsonline/validation/validators.py +561 -0
  146. createsonline/views.py +184 -0
  147. createsonline-0.1.26.dist-info/METADATA +46 -0
  148. createsonline-0.1.26.dist-info/RECORD +152 -0
  149. createsonline-0.1.26.dist-info/WHEEL +5 -0
  150. createsonline-0.1.26.dist-info/entry_points.txt +2 -0
  151. createsonline-0.1.26.dist-info/licenses/LICENSE +21 -0
  152. createsonline-0.1.26.dist-info/top_level.txt +1 -0
@@ -0,0 +1,482 @@
1
+ # createsonline/admin/integration.py
2
+ """
3
+ CREATESONLINE Admin Integration
4
+
5
+ Integrates all admin components: CRUD, permissions, content, dashboard.
6
+ This file extends the existing AdminSite with new features.
7
+ """
8
+ from typing import Dict, Any, Optional
9
+ import os
10
+
11
+
12
+ async def integrate_auto_crud(admin_site, request):
13
+ """
14
+ Integrate auto-CRUD views into admin site
15
+
16
+ This function adds routes for all registered models:
17
+ - /admin/{model}/ - List view
18
+ - /admin/{model}/add - Create view
19
+ - /admin/{model}/{id}/edit - Edit view
20
+ - /admin/{model}/{id}/delete - Delete view
21
+ - /admin/create-model - Model creator
22
+ - /admin/model-manager/{model} - Model structure manager
23
+ """
24
+ from createsonline.admin.crud import ListView, CreateView, EditView, DeleteView
25
+ from createsonline.admin.user_forms import UserCreateForm, UserEditForm
26
+ from createsonline.admin.model_creator import ModelCreator
27
+ from createsonline.admin.model_manager import ModelManager
28
+ from createsonline.auth.models import User
29
+
30
+ # Get URL path
31
+ path = getattr(request, 'path', '/')
32
+ method = getattr(request, 'method', 'GET')
33
+
34
+ # Parse URL pattern: /admin/{model}/{id?}/{action?}
35
+ parts = [p for p in path.split('/') if p]
36
+
37
+ if len(parts) < 2:
38
+ return None
39
+
40
+ if parts[0] != 'admin':
41
+ return None
42
+
43
+ # Check if this is the model manager route
44
+ if parts[1] == 'model-manager' and len(parts) >= 3:
45
+ model_name = parts[2]
46
+
47
+
48
+ # Find registered model
49
+ model_class = None
50
+ for registered_name, registered_admin in admin_site._registry.items():
51
+ if registered_name.lower() == model_name.lower():
52
+ model_class = registered_admin.model
53
+ break
54
+
55
+ if model_class:
56
+ # Check if this is add field route
57
+ if len(parts) >= 5 and parts[3] == 'field' and parts[4] == 'add':
58
+ from createsonline.admin.field_builder import render_add_field_form
59
+ html = render_add_field_form(model_class.__name__)
60
+ from createsonline.admin.interface import InternalHTMLResponse
61
+ return InternalHTMLResponse(html)
62
+
63
+ # Default: show model structure
64
+ manager = ModelManager(model_class, admin_site)
65
+ html = await manager.render(request)
66
+ from createsonline.admin.interface import InternalHTMLResponse
67
+ return InternalHTMLResponse(html)
68
+ else:
69
+ return None
70
+
71
+ # Check if this is the model creator route
72
+ if parts[1] == 'create-model':
73
+ creator = ModelCreator(admin_site)
74
+
75
+ if method == 'POST':
76
+ data = await parse_form_data(request)
77
+ success, message = await creator.create_model(data)
78
+
79
+ if success:
80
+ # Show success page
81
+ from createsonline.admin.interface import InternalHTMLResponse
82
+ success_html = f"""<!DOCTYPE html>
83
+ <html>
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <title>Model Created - CREATESONLINE Admin</title>
87
+ <style>
88
+ body {{
89
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
90
+ background: #0a0a0a;
91
+ color: #ffffff;
92
+ padding: 40px;
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ min-height: 100vh;
97
+ }}
98
+ .success-box {{
99
+ background: #1a1a1a;
100
+ border: 1px solid #2a2a2a;
101
+ border-radius: 12px;
102
+ padding: 40px;
103
+ max-width: 600px;
104
+ text-align: center;
105
+ }}
106
+ h1 {{
107
+ color: #4CAF50;
108
+ margin-bottom: 20px;
109
+ }}
110
+ p {{
111
+ color: #aaa;
112
+ margin-bottom: 30px;
113
+ }}
114
+ .btn {{
115
+ padding: 12px 30px;
116
+ background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
117
+ color: #0a0a0a;
118
+ text-decoration: none;
119
+ border-radius: 8px;
120
+ font-weight: 600;
121
+ display: inline-block;
122
+ }}
123
+ </style>
124
+ </head>
125
+ <body>
126
+ <div class="success-box">
127
+ <h1>✓ Success!</h1>
128
+ <p>{message}</p>
129
+ <p>Next steps:<br>
130
+ 1. Run <code>createsonline-admin migrate</code> to create the database table<br>
131
+ 2. Register the model in your admin.py file<br>
132
+ 3. Refresh the admin to see your new model</p>
133
+ <a href="/admin" class="btn">Back to Admin</a>
134
+ </div>
135
+ </body>
136
+ </html>"""
137
+ return InternalHTMLResponse(success_html)
138
+ else:
139
+ html = await creator.render_form(errors={'model_name': message})
140
+ from createsonline.admin.interface import InternalHTMLResponse
141
+ return InternalHTMLResponse(html)
142
+ else:
143
+ html = await creator.render_form()
144
+ from createsonline.admin.interface import InternalHTMLResponse
145
+ return InternalHTMLResponse(html)
146
+
147
+ model_name = parts[1]
148
+
149
+ # Find registered model
150
+ model_admin = None
151
+ model_class = None
152
+
153
+ for registered_name, registered_admin in admin_site._registry.items():
154
+ if registered_name.lower() == model_name.lower():
155
+ model_admin = registered_admin
156
+ model_class = registered_admin.model
157
+ break
158
+
159
+ if not model_class:
160
+ return None
161
+
162
+ # Get database session
163
+ session = get_database_session()
164
+ if not session:
165
+ return None
166
+
167
+ # Check if this is User model - use custom forms
168
+ is_user_model = model_class == User
169
+
170
+ # Route to appropriate view
171
+ try:
172
+ if len(parts) == 2:
173
+ # List view
174
+ list_view = ListView(model_class, session, admin_site)
175
+
176
+ # Get query parameters
177
+ page = int(getattr(request, 'query_params', {}).get('page', 1))
178
+ search = getattr(request, 'query_params', {}).get('search', '')
179
+
180
+ html = await list_view.render(request, page=page, search=search)
181
+
182
+ from createsonline.admin.interface import InternalHTMLResponse
183
+ return InternalHTMLResponse(html)
184
+
185
+ elif len(parts) == 3 and parts[2] == 'add':
186
+ # Create view - use custom form for User model
187
+ if is_user_model:
188
+ create_view = UserCreateForm(session, admin_site)
189
+ else:
190
+ create_view = CreateView(model_class, session, admin_site)
191
+
192
+ if method == 'POST':
193
+ # Parse form data
194
+ data = await parse_form_data(request)
195
+ success, obj, errors = await create_view.save(request, data)
196
+
197
+ if success:
198
+ # Show success message if password was generated
199
+ if is_user_model and hasattr(obj, '_generated_password'):
200
+ # For now, redirect with success - could add flash message later
201
+ pass
202
+
203
+ # Redirect to list view
204
+ from createsonline.admin.interface import InternalResponse
205
+ return InternalResponse(
206
+ b'',
207
+ status_code=302,
208
+ headers={'location': f'/admin/{model_name}'}
209
+ )
210
+ else:
211
+ html = await create_view.render(request, errors=errors, data=data)
212
+ from createsonline.admin.interface import InternalHTMLResponse
213
+ return InternalHTMLResponse(html)
214
+ else:
215
+ html = await create_view.render(request)
216
+ from createsonline.admin.interface import InternalHTMLResponse
217
+ return InternalHTMLResponse(html)
218
+
219
+ elif len(parts) == 4:
220
+ obj_id = int(parts[2])
221
+ action = parts[3]
222
+
223
+ if action == 'edit':
224
+ # Edit view - use custom form for User model
225
+ if is_user_model:
226
+ edit_view = UserEditForm(obj_id, session, admin_site)
227
+ else:
228
+ edit_view = EditView(model_class, session, admin_site)
229
+
230
+ if method == 'POST':
231
+ data = await parse_form_data(request)
232
+ success, obj, errors = await edit_view.save(request, data)
233
+
234
+ if success:
235
+ from createsonline.admin.interface import InternalResponse
236
+ return InternalResponse(
237
+ b'',
238
+ status_code=302,
239
+ headers={'location': f'/admin/{model_name}'}
240
+ )
241
+ else:
242
+ html = await edit_view.render(request, errors=errors)
243
+ from createsonline.admin.interface import InternalHTMLResponse
244
+ return InternalHTMLResponse(html)
245
+ else:
246
+ html = await edit_view.render(request)
247
+ from createsonline.admin.interface import InternalHTMLResponse
248
+ return InternalHTMLResponse(html)
249
+
250
+ elif action == 'delete':
251
+ # Delete view
252
+ delete_view = DeleteView(model_class, session, admin_site)
253
+
254
+ if method == 'POST':
255
+ success, message = await delete_view.delete(request, obj_id)
256
+
257
+ from createsonline.admin.interface import InternalResponse
258
+ return InternalResponse(
259
+ b'',
260
+ status_code=302,
261
+ headers={'location': f'/admin/{model_name}'}
262
+ )
263
+ else:
264
+ html = await delete_view.render(request, obj_id)
265
+ from createsonline.admin.interface import InternalHTMLResponse
266
+ return InternalHTMLResponse(html)
267
+
268
+ except Exception as e:
269
+ import traceback
270
+ traceback.print_exc()
271
+ return None
272
+
273
+ return None
274
+
275
+
276
+ def get_database_session():
277
+ """Get database session"""
278
+ try:
279
+ from sqlalchemy import create_engine
280
+ from sqlalchemy.orm import sessionmaker
281
+
282
+ database_url = os.getenv("DATABASE_URL", "sqlite:///./createsonline.db")
283
+ engine = create_engine(database_url, echo=False)
284
+ SessionLocal = sessionmaker(bind=engine)
285
+
286
+ return SessionLocal()
287
+ except Exception as e:
288
+ return None
289
+
290
+
291
+ async def parse_form_data(request) -> Dict[str, Any]:
292
+ """Parse form data from request"""
293
+ try:
294
+ from urllib.parse import unquote_plus
295
+
296
+ content_type = getattr(request, 'headers', {}).get('content-type', '')
297
+
298
+ if 'application/json' in content_type:
299
+ return await request.json()
300
+ else:
301
+ # Parse form-urlencoded
302
+ body = await request.body() if hasattr(request, 'body') else b''
303
+ data = {}
304
+
305
+ if body:
306
+ body_str = body.decode('utf-8')
307
+ for pair in body_str.split('&'):
308
+ if '=' in pair:
309
+ key, value = pair.split('=', 1)
310
+ data[unquote_plus(key)] = unquote_plus(value)
311
+
312
+ return data
313
+ except Exception as e:
314
+ return {}
315
+
316
+
317
+ async def load_user_from_database(username: str, password: str):
318
+ """
319
+ Load and authenticate user from database
320
+
321
+ Returns:
322
+ User object if authenticated, None otherwise
323
+ """
324
+ try:
325
+ from createsonline.auth.models import User
326
+
327
+ session = get_database_session()
328
+ if not session:
329
+ return None
330
+
331
+ try:
332
+ user = session.query(User).filter_by(username=username).first()
333
+
334
+ if user and user.is_staff and user.verify_password(password):
335
+ # Record login
336
+ user.record_login_attempt(True)
337
+ session.commit()
338
+
339
+ return user
340
+ elif user:
341
+ # Record failed attempt
342
+ user.record_login_attempt(False)
343
+ session.commit()
344
+
345
+ return None
346
+ finally:
347
+ session.close()
348
+
349
+ except Exception as e:
350
+ return None
351
+
352
+
353
+ async def enhance_dashboard(admin_site, request, user):
354
+ """
355
+ Render enhanced dashboard with insights
356
+
357
+ Args:
358
+ admin_site: AdminSite instance
359
+ request: Request object
360
+ user: Authenticated user
361
+
362
+ Returns:
363
+ HTML response with dashboard
364
+ """
365
+ try:
366
+ from createsonline.admin.modern_dashboard import ModernAdminDashboard
367
+ from createsonline.admin.interface import InternalHTMLResponse
368
+
369
+ dashboard = ModernAdminDashboard(admin_site)
370
+ html = await dashboard.render(request, user)
371
+
372
+ return InternalHTMLResponse(html)
373
+
374
+ except Exception as e:
375
+ import traceback
376
+ traceback.print_exc()
377
+ # Fallback to default dashboard
378
+ return await admin_site._show_dashboard(request)
379
+
380
+
381
+ def check_permissions(user, permission: str) -> bool:
382
+ """
383
+ Check if user has permission
384
+
385
+ Args:
386
+ user: User object
387
+ permission: Permission string like "auth.add_user"
388
+
389
+ Returns:
390
+ True if user has permission
391
+ """
392
+ if not user:
393
+ return False
394
+
395
+ if hasattr(user, 'is_superuser') and user.is_superuser:
396
+ return True
397
+
398
+ if hasattr(user, 'has_permission'):
399
+ return user.has_permission(permission)
400
+
401
+ return False
402
+
403
+
404
+ async def show_permission_denied(request, message: str = "Permission denied"):
405
+ """Show permission denied page"""
406
+ html = f"""
407
+ <!DOCTYPE html>
408
+ <html lang="en">
409
+ <head>
410
+ <meta charset="UTF-8">
411
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
412
+ <title>Permission Denied - CREATESONLINE Admin</title>
413
+ <style>
414
+ * {{
415
+ margin: 0;
416
+ padding: 0;
417
+ box-sizing: border-box;
418
+ }}
419
+
420
+ body {{
421
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
422
+ background: #0a0a0a;
423
+ color: #ffffff;
424
+ display: flex;
425
+ align-items: center;
426
+ justify-content: center;
427
+ min-height: 100vh;
428
+ padding: 20px;
429
+ }}
430
+
431
+ .container {{
432
+ max-width: 600px;
433
+ background: #1a1a1a;
434
+ padding: 50px;
435
+ border-radius: 12px;
436
+ border: 1px solid #2a2a2a;
437
+ text-align: center;
438
+ }}
439
+
440
+ .error-icon {{
441
+ font-size: 5em;
442
+ margin-bottom: 20px;
443
+ }}
444
+
445
+ h1 {{
446
+ font-size: 2.5em;
447
+ background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
448
+ -webkit-background-clip: text;
449
+ -webkit-text-fill-color: transparent;
450
+ margin-bottom: 20px;
451
+ }}
452
+
453
+ p {{
454
+ color: #b0b0b0;
455
+ font-size: 1.2em;
456
+ margin-bottom: 30px;
457
+ }}
458
+
459
+ .btn {{
460
+ padding: 12px 30px;
461
+ background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%);
462
+ color: #0a0a0a;
463
+ border: none;
464
+ border-radius: 8px;
465
+ text-decoration: none;
466
+ font-weight: 600;
467
+ display: inline-block;
468
+ }}
469
+ </style>
470
+ </head>
471
+ <body>
472
+ <div class="container">
473
+ <div class="error-icon">🔒</div>
474
+ <h1>Permission Denied</h1>
475
+ <p>{message}</p>
476
+ <a href="/admin" class="btn">Go to Dashboard</a>
477
+ </div>
478
+ </body>
479
+ </html>
480
+ """
481
+ from createsonline.admin.interface import InternalHTMLResponse
482
+ return InternalHTMLResponse(html, status_code=403)