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,349 @@
1
+ # createsonline/security/encryption.py
2
+ """
3
+ CREATESONLINE Encryption Utilities
4
+
5
+ Military-grade encryption and password hashing:
6
+ - Argon2 password hashing
7
+ - AES-256 encryption
8
+ - Secure token generation
9
+ - Key derivation functions
10
+ """
11
+
12
+ import hashlib
13
+ import hmac
14
+ import secrets
15
+ import base64
16
+ import time
17
+ from typing import Optional, Tuple, Dict, Any
18
+
19
+
20
+ class SecureHasher:
21
+ """
22
+ Military-grade password hashing using Argon2-equivalent security
23
+ Falls back to PBKDF2 if Argon2 not available
24
+ """
25
+
26
+ def __init__(self):
27
+ # Try to import argon2 for best security
28
+ try:
29
+ import argon2
30
+ self.argon2_available = True
31
+ self.argon2_hasher = argon2.PasswordHasher(
32
+ time_cost=3, # Number of iterations
33
+ memory_cost=65536, # Memory usage in KiB
34
+ parallelism=1, # Number of parallel threads
35
+ hash_len=32, # Hash length
36
+ salt_len=16 # Salt length
37
+ )
38
+ pass
39
+ except ImportError:
40
+ self.argon2_available = False
41
+ pass
42
+
43
+ def hash_password(self, password: str) -> str:
44
+ """Hash password with maximum security"""
45
+
46
+ if self.argon2_available:
47
+ # Use Argon2 (recommended by OWASP)
48
+ return self.argon2_hasher.hash(password)
49
+ else:
50
+ # Fallback to PBKDF2 with high iteration count
51
+ salt = secrets.token_bytes(32)
52
+ iterations = 100000 # High iteration count for security
53
+
54
+ # Create PBKDF2 hash
55
+ password_hash = hashlib.pbkdf2_hmac(
56
+ 'sha256',
57
+ password.encode('utf-8'),
58
+ salt,
59
+ iterations
60
+ )
61
+
62
+ # Encode as: algorithm$iterations$salt$hash
63
+ encoded_salt = base64.b64encode(salt).decode('ascii')
64
+ encoded_hash = base64.b64encode(password_hash).decode('ascii')
65
+
66
+ return f"pbkdf2_sha256${iterations}${encoded_salt}${encoded_hash}"
67
+
68
+ def verify_password(self, password: str, hashed: str) -> bool:
69
+ """Verify password against hash"""
70
+
71
+ if self.argon2_available and not hashed.startswith('pbkdf2_'):
72
+ # Verify with Argon2
73
+ try:
74
+ self.argon2_hasher.verify(hashed, password)
75
+ return True
76
+ except:
77
+ return False
78
+ else:
79
+ # Verify with PBKDF2
80
+ try:
81
+ parts = hashed.split('$')
82
+ if len(parts) != 4 or parts[0] != 'pbkdf2_sha256':
83
+ return False
84
+
85
+ iterations = int(parts[1])
86
+ salt = base64.b64decode(parts[2])
87
+ stored_hash = base64.b64decode(parts[3])
88
+
89
+ # Compute hash with same parameters
90
+ computed_hash = hashlib.pbkdf2_hmac(
91
+ 'sha256',
92
+ password.encode('utf-8'),
93
+ salt,
94
+ iterations
95
+ )
96
+
97
+ # Constant-time comparison to prevent timing attacks
98
+ return hmac.compare_digest(stored_hash, computed_hash)
99
+
100
+ except (ValueError, IndexError):
101
+ return False
102
+
103
+ def needs_rehash(self, hashed: str) -> bool:
104
+ """Check if password needs rehashing with stronger parameters"""
105
+
106
+ if self.argon2_available:
107
+ if not hashed.startswith('pbkdf2_'):
108
+ # Already using Argon2
109
+ try:
110
+ return self.argon2_hasher.check_needs_rehash(hashed)
111
+ except:
112
+ return True
113
+ else:
114
+ # Upgrade from PBKDF2 to Argon2
115
+ return True
116
+ else:
117
+ # Check PBKDF2 iteration count
118
+ try:
119
+ parts = hashed.split('$')
120
+ if len(parts) == 4 and parts[0] == 'pbkdf2_sha256':
121
+ iterations = int(parts[1])
122
+ return iterations < 100000 # Upgrade if less than 100k iterations
123
+ except:
124
+ pass
125
+
126
+ return True
127
+
128
+
129
+ class SecureTokenGenerator:
130
+ """Generate cryptographically secure tokens"""
131
+
132
+ @staticmethod
133
+ def generate_token(length: int = 32) -> str:
134
+ """Generate secure random token"""
135
+ return secrets.token_urlsafe(length)
136
+
137
+ @staticmethod
138
+ def generate_api_key() -> str:
139
+ """Generate API key with prefix"""
140
+ prefix = "cos_" # CREATESONLINE prefix
141
+ token = secrets.token_urlsafe(32)
142
+ return f"{prefix}{token}"
143
+
144
+ @staticmethod
145
+ def generate_session_id() -> str:
146
+ """Generate session ID"""
147
+ timestamp = str(int(time.time()))
148
+ random_part = secrets.token_urlsafe(24)
149
+ return f"{timestamp}_{random_part}"
150
+
151
+ @staticmethod
152
+ def generate_csrf_token(session_id: str, secret_key: str) -> str:
153
+ """Generate CSRF token tied to session"""
154
+ timestamp = str(int(time.time()))
155
+ message = f"{session_id}:{timestamp}"
156
+
157
+ signature = hmac.new(
158
+ secret_key.encode(),
159
+ message.encode(),
160
+ hashlib.sha256
161
+ ).hexdigest()
162
+
163
+ return f"{timestamp}:{signature}"
164
+
165
+ @staticmethod
166
+ def verify_csrf_token(token: str, session_id: str, secret_key: str,
167
+ max_age: int = 3600) -> bool:
168
+ """Verify CSRF token"""
169
+ try:
170
+ timestamp_str, signature = token.split(':', 1)
171
+ timestamp = int(timestamp_str)
172
+
173
+ # Check token age
174
+ if time.time() - timestamp > max_age:
175
+ return False
176
+
177
+ # Verify signature
178
+ message = f"{session_id}:{timestamp_str}"
179
+ expected_signature = hmac.new(
180
+ secret_key.encode(),
181
+ message.encode(),
182
+ hashlib.sha256
183
+ ).hexdigest()
184
+
185
+ return hmac.compare_digest(signature, expected_signature)
186
+
187
+ except (ValueError, IndexError):
188
+ return False
189
+
190
+
191
+ class AESEncryption:
192
+ """AES-256 encryption utilities"""
193
+
194
+ def __init__(self, key: Optional[bytes] = None):
195
+ if key is None:
196
+ key = secrets.token_bytes(32) # 256-bit key
197
+ elif len(key) != 32:
198
+ # Derive 256-bit key from provided key
199
+ key = hashlib.sha256(key).digest()
200
+
201
+ self.key = key
202
+
203
+ # Try to import cryptography library for AES
204
+ try:
205
+ from cryptography.fernet import Fernet
206
+ from cryptography.hazmat.primitives import hashes
207
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
208
+
209
+ # Create Fernet key from our key
210
+ kdf = PBKDF2HMAC(
211
+ algorithm=hashes.SHA256(),
212
+ length=32,
213
+ salt=b'createsonline_salt', # In production, use random salt
214
+ iterations=100000,
215
+ )
216
+ fernet_key = base64.urlsafe_b64encode(kdf.derive(self.key))
217
+ self.cipher = Fernet(fernet_key)
218
+ self.crypto_available = True
219
+ pass
220
+
221
+ except ImportError:
222
+ self.crypto_available = False
223
+ pass
224
+
225
+ def encrypt(self, data: str) -> str:
226
+ """Encrypt string data"""
227
+
228
+ if self.crypto_available:
229
+ # Use Fernet (AES-256)
230
+ encrypted = self.cipher.encrypt(data.encode())
231
+ return base64.b64encode(encrypted).decode()
232
+ else:
233
+ # Fallback to XOR encryption
234
+ data_bytes = data.encode()
235
+ encrypted_bytes = bytearray()
236
+
237
+ for i, byte in enumerate(data_bytes):
238
+ key_byte = self.key[i % len(self.key)]
239
+ encrypted_bytes.append(byte ^ key_byte)
240
+
241
+ return base64.b64encode(encrypted_bytes).decode()
242
+
243
+ def decrypt(self, encrypted_data: str) -> str:
244
+ """Decrypt string data"""
245
+
246
+ try:
247
+ if self.crypto_available:
248
+ # Use Fernet (AES-256)
249
+ encrypted_bytes = base64.b64decode(encrypted_data)
250
+ decrypted = self.cipher.decrypt(encrypted_bytes)
251
+ return decrypted.decode()
252
+ else:
253
+ # Fallback to XOR decryption
254
+ encrypted_bytes = base64.b64decode(encrypted_data)
255
+ decrypted_bytes = bytearray()
256
+
257
+ for i, byte in enumerate(encrypted_bytes):
258
+ key_byte = self.key[i % len(self.key)]
259
+ decrypted_bytes.append(byte ^ key_byte)
260
+
261
+ return decrypted_bytes.decode()
262
+
263
+ except Exception:
264
+ raise ValueError("Invalid encrypted data")
265
+
266
+ def encrypt_dict(self, data: Dict[str, Any]) -> str:
267
+ """Encrypt dictionary as JSON"""
268
+ import json
269
+ json_str = json.dumps(data, separators=(',', ':'))
270
+ return self.encrypt(json_str)
271
+
272
+ def decrypt_dict(self, encrypted_data: str) -> Dict[str, Any]:
273
+ """Decrypt to dictionary"""
274
+ import json
275
+ json_str = self.decrypt(encrypted_data)
276
+ return json.loads(json_str)
277
+
278
+
279
+ class KeyDerivation:
280
+ """Key derivation functions for secure key generation"""
281
+
282
+ @staticmethod
283
+ def derive_key(password: str, salt: Optional[bytes] = None,
284
+ length: int = 32, iterations: int = 100000) -> Tuple[bytes, bytes]:
285
+ """Derive encryption key from password"""
286
+
287
+ if salt is None:
288
+ salt = secrets.token_bytes(32)
289
+
290
+ key = hashlib.pbkdf2_hmac(
291
+ 'sha256',
292
+ password.encode(),
293
+ salt,
294
+ iterations,
295
+ dklen=length
296
+ )
297
+
298
+ return key, salt
299
+
300
+ @staticmethod
301
+ def derive_multiple_keys(master_key: bytes, labels: list,
302
+ length: int = 32) -> Dict[str, bytes]:
303
+ """Derive multiple keys from master key using HKDF-like approach"""
304
+
305
+ keys = {}
306
+
307
+ for label in labels:
308
+ # Create unique key for each label
309
+ info = f"CREATESONLINE_{label}".encode()
310
+
311
+ # Simple HKDF-like derivation
312
+ prk = hmac.new(b'salt', master_key, hashlib.sha256).digest()
313
+ okm = hmac.new(prk, info + b'\x01', hashlib.sha256).digest()[:length]
314
+
315
+ keys[label] = okm
316
+
317
+ return keys
318
+
319
+
320
+ # Global secure hasher instance
321
+ _secure_hasher = SecureHasher()
322
+
323
+ def encrypt_password(password: str) -> str:
324
+ """Encrypt password with maximum security"""
325
+ return _secure_hasher.hash_password(password)
326
+
327
+ def verify_password(password: str, hashed: str) -> bool:
328
+ """Verify password against hash"""
329
+ return _secure_hasher.verify_password(password, hashed)
330
+
331
+ def generate_secure_token(length: int = 32) -> str:
332
+ """Generate cryptographically secure token"""
333
+ return SecureTokenGenerator.generate_token(length)
334
+
335
+ def generate_api_key() -> str:
336
+ """Generate API key"""
337
+ return SecureTokenGenerator.generate_api_key()
338
+
339
+ def generate_session_id() -> str:
340
+ """Generate session ID"""
341
+ return SecureTokenGenerator.generate_session_id()
342
+
343
+ def create_encryption_cipher(key: Optional[bytes] = None) -> AESEncryption:
344
+ """Create AES encryption cipher"""
345
+ return AESEncryption(key)
346
+
347
+ def secure_compare(a: str, b: str) -> bool:
348
+ """Constant-time string comparison to prevent timing attacks"""
349
+ return hmac.compare_digest(a.encode(), b.encode())
@@ -0,0 +1,295 @@
1
+ # createsonline/server.py
2
+ """
3
+ CREATESONLINE Pure Python HTTP Server
4
+
5
+ Zero external dependencies - runs ASGI apps with just Python stdlib.
6
+ This eliminates the need for uvicorn.
7
+ """
8
+
9
+ import asyncio
10
+ import socket
11
+ import json
12
+ import time
13
+ from datetime import datetime
14
+ from typing import Callable
15
+ from urllib.parse import parse_qs, urlparse
16
+ import logging
17
+
18
+ logger = logging.getLogger("createsonline.server")
19
+
20
+ class CreatesonlineServer:
21
+ """Pure Python HTTP server for ASGI applications"""
22
+
23
+ def __init__(self, app: Callable, host: str = "127.0.0.1", port: int = 8000):
24
+ self.app = app
25
+ self.host = host
26
+ self.original_port = port
27
+ self.port = self._find_available_port(host, port)
28
+ self.server = None
29
+
30
+ # Notify if port changed
31
+ if self.port != self.original_port:
32
+ import logging
33
+ logger = logging.getLogger("createsonline")
34
+ logger.warning(f"Port {self.original_port} is busy, using port {self.port} instead")
35
+
36
+ def _find_available_port(self, host: str, start_port: int, max_attempts: int = 100) -> int:
37
+ """Find an available port starting from start_port"""
38
+ for port in range(start_port, start_port + max_attempts):
39
+ try:
40
+ # Try to bind to the port
41
+ test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
42
+ test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
43
+ test_socket.bind((host, port))
44
+ test_socket.close()
45
+ return port
46
+ except OSError:
47
+ # Port is in use, try next one
48
+ continue
49
+
50
+ # If no port found, raise error
51
+ raise RuntimeError(f"No available ports found between {start_port} and {start_port + max_attempts}")
52
+
53
+ async def handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
54
+ """Handle individual client connection"""
55
+ start_time = time.time()
56
+ response_size = 0
57
+ status_code = 500
58
+
59
+ try:
60
+ # Read HTTP request
61
+ request_line = await reader.readline()
62
+ if not request_line:
63
+ return
64
+
65
+ request_line = request_line.decode('utf-8').strip()
66
+ method, path, _ = request_line.split(' ', 2)
67
+
68
+ # Read headers
69
+ headers = {}
70
+ while True:
71
+ line = await reader.readline()
72
+ if line == b'\r\n':
73
+ break
74
+ if line:
75
+ key, value = line.decode('utf-8').strip().split(':', 1)
76
+ headers[key.lower()] = value.strip()
77
+
78
+ # Read body if present
79
+ body = b''
80
+ if 'content-length' in headers:
81
+ content_length = int(headers['content-length'])
82
+ body = await reader.readexactly(content_length)
83
+
84
+ # Parse URL
85
+ parsed_url = urlparse(path)
86
+ query_string = parsed_url.query.encode() if parsed_url.query else b''
87
+
88
+ # Build ASGI scope
89
+ scope = {
90
+ 'type': 'http',
91
+ 'asgi': {'version': '3.0'},
92
+ 'http_version': '1.1',
93
+ 'method': method,
94
+ 'scheme': 'http',
95
+ 'path': parsed_url.path,
96
+ 'query_string': query_string,
97
+ 'root_path': '',
98
+ 'headers': [[k.encode(), v.encode()] for k, v in headers.items()],
99
+ 'server': (self.host, self.port),
100
+ }
101
+
102
+ # ASGI receive callable
103
+ body_sent = False
104
+ async def receive():
105
+ nonlocal body_sent
106
+ if not body_sent:
107
+ body_sent = True
108
+ return {
109
+ 'type': 'http.request',
110
+ 'body': body,
111
+ 'more_body': False,
112
+ }
113
+ return {'type': 'http.disconnect'}
114
+
115
+ # ASGI send callable
116
+ response_started = False
117
+ async def send(message):
118
+ nonlocal response_started, response_size, status_code
119
+
120
+ if message['type'] == 'http.response.start':
121
+ response_started = True
122
+ status_code = message['status']
123
+ headers = message.get('headers', [])
124
+
125
+ # Write status line
126
+ writer.write(f'HTTP/1.1 {status_code} OK\r\n'.encode())
127
+
128
+ # Write headers
129
+ for name, value in headers:
130
+ writer.write(f'{name.decode()}: {value.decode()}\r\n'.encode())
131
+ writer.write(b'\r\n')
132
+
133
+ elif message['type'] == 'http.response.body':
134
+ body = message.get('body', b'')
135
+ if body:
136
+ response_size += len(body)
137
+ writer.write(body)
138
+ await writer.drain()
139
+
140
+ # Call ASGI app
141
+ await self.app(scope, receive, send)
142
+
143
+ # Log request after completion
144
+ elapsed_ms = (time.time() - start_time) * 1000
145
+ timestamp = datetime.now().strftime("%H:%M:%S")
146
+ status_indicator = self._get_status_indicator(status_code)
147
+
148
+ # Format size
149
+ if response_size < 1024:
150
+ size_str = f"{response_size}B"
151
+ elif response_size < 1024 * 1024:
152
+ size_str = f"{response_size / 1024:.1f}KB"
153
+ else:
154
+ size_str = f"{response_size / (1024 * 1024):.1f}MB"
155
+
156
+ # Color-coded status for better visibility
157
+ status_display = f"{status_code}"
158
+ logger.info(f"[{timestamp}] [{status_indicator}] {method:<6} {path:<50} {status_display:<4} {size_str:>8} {elapsed_ms:>5.0f}ms")
159
+ except Exception as e:
160
+ # Log error to console
161
+ elapsed_ms = (time.time() - start_time) * 1000
162
+ timestamp = datetime.now().strftime("%H:%M:%S")
163
+ logger.error(f"[{timestamp}] [XXX] {method:<6} {path:<50} 500 ERROR {elapsed_ms:>5.0f}ms | {str(e)}")
164
+ # Send 500 error
165
+ error_response = json.dumps({
166
+ "error": "Internal Server Error",
167
+ "message": str(e)
168
+ }).encode()
169
+
170
+ response = (
171
+ b'HTTP/1.1 500 Internal Server Error\r\n'
172
+ b'Content-Type: application/json\r\n'
173
+ b'Content-Length: ' + str(len(error_response)).encode() + b'\r\n'
174
+ b'\r\n'
175
+ ) + error_response
176
+
177
+ writer.write(response)
178
+ await writer.drain()
179
+
180
+ finally:
181
+ writer.close()
182
+ await writer.wait_closed()
183
+
184
+ def _get_status_indicator(self, status: int) -> str:
185
+ """Get visual indicator for HTTP status code"""
186
+ if 200 <= status < 300:
187
+ return "OK " # Success
188
+ elif 300 <= status < 400:
189
+ return "->>" # Redirect
190
+ elif 400 <= status < 500:
191
+ return "ERR" # Client error
192
+ else:
193
+ return "XXX" # Server error
194
+
195
+ async def serve(self):
196
+ """Start the server"""
197
+ try:
198
+ self.server = await asyncio.start_server(
199
+ self.handle_client,
200
+ self.host,
201
+ self.port
202
+ )
203
+
204
+ # Print server info
205
+ from . import __version__
206
+ startup_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
207
+ print("\n" + "=" * 70)
208
+ print(f" CREATESONLINE v{__version__} - AI-Native Web Framework")
209
+ print("=" * 70)
210
+ print(f" Started: {startup_time}")
211
+ print(f" Server URL: http://{self.host}:{self.port}")
212
+ print(f" Press CTRL+C to stop")
213
+ print("=" * 70)
214
+ print(f"\n{'TIME':<12} {'STATUS':<7} {'METHOD':<8} {'PATH':<50} {'CODE':<6} {'SIZE':<10} {'TIME'}")
215
+ print("-" * 120)
216
+ except OSError as e:
217
+ logger.error(f"Failed to start server on {self.host}:{self.port}")
218
+ logger.error(f"Error: {e}")
219
+ raise
220
+
221
+
222
+ async with self.server:
223
+ await self.server.serve_forever()
224
+
225
+ def run(self):
226
+ """Run the server (blocking)"""
227
+ try:
228
+ asyncio.run(self.serve())
229
+ except KeyboardInterrupt:
230
+ logger.info("Server stopped")
231
+ def run_server(app: Callable, host: str = "127.0.0.1", port: int = 8000, reload: bool = False):
232
+ """
233
+ Run CREATESONLINE pure Python server
234
+
235
+ Args:
236
+ app: ASGI application callable
237
+ host: Host to bind to
238
+ port: Port to listen on
239
+ reload: Enable auto-reload on file changes
240
+ """
241
+ # Configure logging
242
+ logging.basicConfig(
243
+ level=logging.INFO,
244
+ format='%(message)s'
245
+ )
246
+
247
+ if reload:
248
+ # Use watchdog for auto-reload
249
+ try:
250
+ import sys
251
+ import subprocess
252
+ from pathlib import Path
253
+ from watchdog.observers import Observer
254
+ from watchdog.events import FileSystemEventHandler
255
+
256
+ class ReloadHandler(FileSystemEventHandler):
257
+ def __init__(self):
258
+ self.process = None
259
+ self.restart_server()
260
+
261
+ def restart_server(self):
262
+ if self.process:
263
+ self.process.terminate()
264
+ self.process.wait()
265
+
266
+ logger.info("🔄 Restarting server...")
267
+ self.process = subprocess.Popen([sys.executable] + sys.argv)
268
+
269
+ def on_modified(self, event):
270
+ if event.src_path.endswith('.py'):
271
+ logger.info(f"📝 File changed: {event.src_path}")
272
+ self.restart_server()
273
+
274
+ handler = ReloadHandler()
275
+ observer = Observer()
276
+ observer.schedule(handler, path=Path.cwd(), recursive=True)
277
+ observer.start()
278
+
279
+ try:
280
+ observer.join()
281
+ except KeyboardInterrupt:
282
+ observer.stop()
283
+ if handler.process:
284
+ handler.process.terminate()
285
+ observer.join()
286
+
287
+ except ImportError:
288
+ logger.warning("⚠️ watchdog not installed, auto-reload disabled")
289
+ logger.info("Install with: pip install watchdog")
290
+ server = CreatesonlineServer(app, host, port)
291
+ server.run()
292
+ else:
293
+ server = CreatesonlineServer(app, host, port)
294
+ server.run()
295
+