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,604 @@
1
+ """
2
+ CREATESONLINE Database Models
3
+ Base model classes that wrap SQLAlchemy with clean API.
4
+ """
5
+ from typing import Any, Dict, List, Optional, Type
6
+ from datetime import datetime
7
+ from sqlalchemy import Column, Integer, DateTime
8
+ from sqlalchemy.ext.declarative import declarative_base
9
+ from .abstraction import Database
10
+
11
+ # SQLAlchemy base
12
+ SQLAlchemyBase = declarative_base()
13
+
14
+ class CreatesonlineModel(SQLAlchemyBase):
15
+ """
16
+ Base model class that provides clean API over SQLAlchemy.
17
+ All CREATESONLINE models should inherit from this.
18
+ """
19
+ __abstract__ = True
20
+
21
+ # Standard fields that every model gets
22
+ id = Column(Integer, primary_key=True, autoincrement=True)
23
+ created_at = Column(DateTime, default=datetime.utcnow)
24
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
25
+
26
+ def __init__(self, **kwargs):
27
+ """Initialize model with field values."""
28
+ for key, value in kwargs.items():
29
+ if hasattr(self, key):
30
+ setattr(self, key, value)
31
+
32
+ @classmethod
33
+ def create(cls, **kwargs) -> 'CreatesonlineModel':
34
+ """Create and save a new instance."""
35
+ instance = cls(**kwargs)
36
+ instance.save()
37
+ return instance
38
+
39
+ @classmethod
40
+ def get(cls, id: int) -> Optional['CreatesonlineModel']:
41
+ """Get instance by ID."""
42
+ db = Database.get_instance()
43
+ with db.session() as session:
44
+ return session.query(cls).filter(cls.id == id).first()
45
+
46
+ @classmethod
47
+ def get_by(cls, **filters) -> Optional['CreatesonlineModel']:
48
+ """Get instance by field filters."""
49
+ db = Database.get_instance()
50
+ with db.session() as session:
51
+ query = session.query(cls)
52
+ for field, value in filters.items():
53
+ if hasattr(cls, field):
54
+ query = query.filter(getattr(cls, field) == value)
55
+ return query.first()
56
+
57
+ @classmethod
58
+ def filter(cls, **filters) -> List['CreatesonlineModel']:
59
+ """Get all instances matching filters."""
60
+ db = Database.get_instance()
61
+ with db.session() as session:
62
+ query = session.query(cls)
63
+ for field, value in filters.items():
64
+ if hasattr(cls, field):
65
+ query = query.filter(getattr(cls, field) == value)
66
+ return query.all()
67
+
68
+ @classmethod
69
+ def all(cls) -> List['CreatesonlineModel']:
70
+ """Get all instances."""
71
+ db = Database.get_instance()
72
+ with db.session() as session:
73
+ return session.query(cls).all()
74
+
75
+ @classmethod
76
+ def count(cls, **filters) -> int:
77
+ """Count instances matching filters."""
78
+ db = Database.get_instance()
79
+ with db.session() as session:
80
+ query = session.query(cls)
81
+ for field, value in filters.items():
82
+ if hasattr(cls, field):
83
+ query = query.filter(getattr(cls, field) == value)
84
+ return query.count()
85
+
86
+ def save(self) -> 'CreatesonlineModel':
87
+ """Save instance to database."""
88
+ from sqlalchemy.orm import object_session
89
+
90
+ db = Database.get_instance()
91
+ session = object_session(self)
92
+
93
+ if session is None:
94
+ with db.session() as new_session:
95
+ new_session.add(self)
96
+ new_session.commit()
97
+ new_session.refresh(self)
98
+ else:
99
+ session.add(self)
100
+ session.commit()
101
+ session.refresh(self)
102
+
103
+ return self
104
+
105
+ def update(self, **kwargs) -> 'CreatesonlineModel':
106
+ """Update instance fields."""
107
+ for key, value in kwargs.items():
108
+ if hasattr(self, key):
109
+ setattr(self, key, value)
110
+
111
+ self.updated_at = datetime.utcnow()
112
+ return self.save()
113
+
114
+ def delete(self) -> bool:
115
+ """Delete instance from database."""
116
+ from sqlalchemy.orm import object_session
117
+
118
+ db = Database.get_instance()
119
+ session = object_session(self)
120
+
121
+ if session is None:
122
+ with db.session() as new_session:
123
+ # Merge the instance into the new session
124
+ merged = new_session.merge(self)
125
+ new_session.delete(merged)
126
+ new_session.commit()
127
+ else:
128
+ session.delete(self)
129
+ session.commit()
130
+
131
+ return True
132
+
133
+ def to_dict(self, exclude: Optional[List[str]] = None) -> Dict[str, Any]:
134
+ """Convert instance to dictionary."""
135
+ exclude = exclude or []
136
+ result = {}
137
+
138
+ for column in self.__table__.columns:
139
+ field_name = column.name
140
+ if field_name not in exclude:
141
+ value = getattr(self, field_name)
142
+
143
+ # Handle datetime serialization
144
+ if isinstance(value, datetime):
145
+ value = value.isoformat()
146
+
147
+ result[field_name] = value
148
+
149
+ return result
150
+
151
+ def from_dict(self, data: Dict[str, Any]) -> 'CreatesonlineModel':
152
+ """Update instance from dictionary."""
153
+ for key, value in data.items():
154
+ if hasattr(self, key) and key not in ['id', 'created_at']:
155
+ setattr(self, key, value)
156
+ return self
157
+
158
+ @classmethod
159
+ def bulk_create(cls, instances_data: List[Dict[str, Any]]) -> List['CreatesonlineModel']:
160
+ """Create multiple instances efficiently."""
161
+ db = Database.get_instance()
162
+ instances = []
163
+
164
+ with db.session() as session:
165
+ for data in instances_data:
166
+ instance = cls(**data)
167
+ session.add(instance)
168
+ instances.append(instance)
169
+
170
+ session.commit()
171
+
172
+ # Refresh all instances to get IDs
173
+ for instance in instances:
174
+ session.refresh(instance)
175
+
176
+ return instances
177
+
178
+ @classmethod
179
+ def bulk_update(cls, updates: List[Dict[str, Any]], id_field: str = 'id') -> int:
180
+ """Update multiple instances efficiently."""
181
+ db = Database.get_instance()
182
+
183
+ with db.session() as session:
184
+ updated_count = 0
185
+
186
+ for update_data in updates:
187
+ if id_field not in update_data:
188
+ continue
189
+
190
+ id_value = update_data.pop(id_field)
191
+ update_data['updated_at'] = datetime.utcnow()
192
+
193
+ result = session.query(cls).filter(
194
+ getattr(cls, id_field) == id_value
195
+ ).update(update_data)
196
+
197
+ updated_count += result
198
+
199
+ session.commit()
200
+ return updated_count
201
+
202
+ def __repr__(self) -> str:
203
+ """String representation of model."""
204
+ return f"<{self.__class__.__name__}(id={getattr(self, 'id', None)})>"
205
+
206
+
207
+ class QueryBuilder:
208
+ """
209
+ Fluent query builder for CREATESONLINE models.
210
+ Provides intuitive query construction.
211
+ """
212
+
213
+ def __init__(self, model_class: Type[CreatesonlineModel]):
214
+ self.model_class = model_class
215
+ self.db = Database.get_instance()
216
+ self._filters = []
217
+ self._order_by = []
218
+ self._limit_value = None
219
+ self._offset_value = None
220
+
221
+ def where(self, field: str, operator: str = '=', value: Any = None) -> 'QueryBuilder':
222
+ """Add WHERE clause."""
223
+ if hasattr(self.model_class, field):
224
+ column = getattr(self.model_class, field)
225
+
226
+ if operator == '=':
227
+ self._filters.append(column == value)
228
+ elif operator == '!=':
229
+ self._filters.append(column != value)
230
+ elif operator == '>':
231
+ self._filters.append(column > value)
232
+ elif operator == '>=':
233
+ self._filters.append(column >= value)
234
+ elif operator == '<':
235
+ self._filters.append(column < value)
236
+ elif operator == '<=':
237
+ self._filters.append(column <= value)
238
+ elif operator == 'like':
239
+ self._filters.append(column.like(value))
240
+ elif operator == 'in':
241
+ self._filters.append(column.in_(value))
242
+ elif operator == 'not_in':
243
+ self._filters.append(~column.in_(value))
244
+ elif operator == 'is_null':
245
+ self._filters.append(column.is_(None))
246
+ elif operator == 'is_not_null':
247
+ self._filters.append(column.is_not(None))
248
+
249
+ return self
250
+
251
+ def order_by(self, field: str, direction: str = 'asc') -> 'QueryBuilder':
252
+ """Add ORDER BY clause."""
253
+ if hasattr(self.model_class, field):
254
+ column = getattr(self.model_class, field)
255
+ if direction.lower() == 'desc':
256
+ self._order_by.append(column.desc())
257
+ else:
258
+ self._order_by.append(column.asc())
259
+
260
+ return self
261
+
262
+ def limit(self, count: int) -> 'QueryBuilder':
263
+ """Add LIMIT clause."""
264
+ self._limit_value = count
265
+ return self
266
+
267
+ def offset(self, count: int) -> 'QueryBuilder':
268
+ """Add OFFSET clause."""
269
+ self._offset_value = count
270
+ return self
271
+
272
+ def _build_query(self, session):
273
+ """Build SQLAlchemy query."""
274
+ query = session.query(self.model_class)
275
+
276
+ # Apply filters
277
+ for filter_condition in self._filters:
278
+ query = query.filter(filter_condition)
279
+
280
+ # Apply ordering
281
+ for order_condition in self._order_by:
282
+ query = query.order_by(order_condition)
283
+
284
+ # Apply offset
285
+ if self._offset_value is not None:
286
+ query = query.offset(self._offset_value)
287
+
288
+ # Apply limit
289
+ if self._limit_value is not None:
290
+ query = query.limit(self._limit_value)
291
+
292
+ return query
293
+
294
+ def get(self) -> List[CreatesonlineModel]:
295
+ """Execute query and return results."""
296
+ with self.db.session() as session:
297
+ query = self._build_query(session)
298
+ return query.all()
299
+
300
+ def first(self) -> Optional[CreatesonlineModel]:
301
+ """Get first result."""
302
+ with self.db.session() as session:
303
+ query = self._build_query(session)
304
+ return query.first()
305
+
306
+ def count(self) -> int:
307
+ """Count matching records."""
308
+ with self.db.session() as session:
309
+ query = session.query(self.model_class)
310
+
311
+ # Apply filters only
312
+ for filter_condition in self._filters:
313
+ query = query.filter(filter_condition)
314
+
315
+ return query.count()
316
+
317
+ def exists(self) -> bool:
318
+ """Check if any records match."""
319
+ return self.count() > 0
320
+
321
+ def delete(self) -> int:
322
+ """Delete matching records."""
323
+ with self.db.session() as session:
324
+ query = session.query(self.model_class)
325
+
326
+ # Apply filters only
327
+ for filter_condition in self._filters:
328
+ query = query.filter(filter_condition)
329
+
330
+ count = query.count()
331
+ query.delete()
332
+ session.commit()
333
+
334
+ return count
335
+
336
+ def update(self, **values) -> int:
337
+ """Update matching records."""
338
+ values['updated_at'] = datetime.utcnow()
339
+
340
+ with self.db.session() as session:
341
+ query = session.query(self.model_class)
342
+
343
+ # Apply filters only
344
+ for filter_condition in self._filters:
345
+ query = query.filter(filter_condition)
346
+
347
+ count = query.update(values)
348
+ session.commit()
349
+
350
+ return count
351
+
352
+
353
+ # Convenience function for query building
354
+ def query(model_class: Type[CreatesonlineModel]) -> QueryBuilder:
355
+ """Create a new query builder for the given model."""
356
+ return QueryBuilder(model_class)
357
+
358
+
359
+ # ========================================
360
+ # ENHANCED AUDIT LOG MODEL
361
+ # ========================================
362
+
363
+ from typing import Dict, Any, Optional, List
364
+ from datetime import datetime, timedelta
365
+ from enum import Enum
366
+ import json
367
+
368
+ class AuditLogType(Enum):
369
+ """Types of operations that can be audited"""
370
+ SELECT = "select"
371
+ INSERT = "insert"
372
+ UPDATE = "update"
373
+ DELETE = "delete"
374
+ CREATE = "create"
375
+ DROP = "drop"
376
+ LOGIN = "login"
377
+ LOGOUT = "logout"
378
+ PASSWORD_CHANGE = "password_change"
379
+ PERMISSION_CHANGE = "permission_change"
380
+
381
+
382
+ class AuditLogStatus(Enum):
383
+ """Status of audited operations"""
384
+ SUCCESS = "success"
385
+ ERROR = "error"
386
+ PENDING = "pending"
387
+ CANCELLED = "cancelled"
388
+
389
+
390
+ class AuditLog:
391
+ """Enhanced audit log model with rollback capabilities"""
392
+
393
+ def __init__(self, db_connection):
394
+ self.db = db_connection
395
+ self._ensure_audit_table()
396
+
397
+ def _ensure_audit_table(self):
398
+ """Ensure enhanced audit_logs table exists"""
399
+ create_sql = f'''
400
+ CREATE TABLE IF NOT EXISTS audit_logs (
401
+ id {'SERIAL' if self.db.db_type == 'postgresql' else 'INTEGER'} PRIMARY KEY{' AUTOINCREMENT' if self.db.db_type == 'sqlite' else ''},
402
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
403
+ operation_type VARCHAR(50) NOT NULL,
404
+ table_name VARCHAR(100),
405
+ record_id INTEGER,
406
+ sql_query TEXT NOT NULL,
407
+ original_prompt TEXT,
408
+ parameters TEXT,
409
+ status VARCHAR(20) NOT NULL,
410
+ rows_affected INTEGER DEFAULT 0,
411
+ error_message TEXT,
412
+ user_id INTEGER REFERENCES createsonline_users(id),
413
+ session_token VARCHAR(128),
414
+ ip_address VARCHAR(45),
415
+ user_agent TEXT,
416
+ rollback_sql TEXT,
417
+ rollback_status VARCHAR(20),
418
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
419
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
420
+ )
421
+ '''
422
+
423
+ try:
424
+ cursor = self.db.connection.cursor()
425
+ cursor.execute(create_sql)
426
+ self.db.connection.commit()
427
+ except Exception as e:
428
+ raise Exception(f"Failed to create enhanced audit_logs table: {e}")
429
+
430
+ def log_operation(
431
+ self,
432
+ operation_type: AuditLogType,
433
+ sql_query: str,
434
+ status: AuditLogStatus,
435
+ table_name: str = None,
436
+ record_id: int = None,
437
+ original_prompt: str = None,
438
+ parameters: Dict[str, Any] = None,
439
+ rows_affected: int = 0,
440
+ error_message: str = None,
441
+ user_id: int = None,
442
+ session_token: str = None,
443
+ ip_address: str = None,
444
+ user_agent: str = None,
445
+ rollback_sql: str = None
446
+ ) -> int:
447
+ """Log an operation to the audit table"""
448
+
449
+ log_entry = {
450
+ 'timestamp': datetime.now().isoformat(),
451
+ 'operation_type': operation_type.value,
452
+ 'table_name': table_name,
453
+ 'record_id': record_id,
454
+ 'sql_query': sql_query,
455
+ 'original_prompt': original_prompt or '',
456
+ 'parameters': json.dumps(parameters) if parameters else None,
457
+ 'status': status.value,
458
+ 'rows_affected': rows_affected,
459
+ 'error_message': error_message or '',
460
+ 'user_id': user_id,
461
+ 'session_token': session_token,
462
+ 'ip_address': ip_address,
463
+ 'user_agent': user_agent,
464
+ 'rollback_sql': rollback_sql,
465
+ 'rollback_status': None
466
+ }
467
+
468
+ try:
469
+ audit_id = self.db.insert('audit_logs', log_entry)
470
+ return audit_id
471
+ except Exception as e:
472
+ # Fallback logging if audit fails
473
+ return None
474
+
475
+ def get_by_id(self, audit_id: int) -> Optional[Dict[str, Any]]:
476
+ """Get audit log entry by ID"""
477
+ try:
478
+ placeholder = self.db._get_placeholder()
479
+ result = self.db.execute(
480
+ f"SELECT * FROM audit_logs WHERE id = {placeholder}",
481
+ (audit_id,)
482
+ )
483
+ return result[0] if result else None
484
+ except Exception:
485
+ return None
486
+
487
+ def get_recent(self, limit: int = 50, operation_type: AuditLogType = None) -> List[Dict[str, Any]]:
488
+ """Get recent audit log entries"""
489
+ try:
490
+ placeholder = self.db._get_placeholder()
491
+
492
+ if operation_type:
493
+ sql = f"""
494
+ SELECT * FROM audit_logs
495
+ WHERE operation_type = {placeholder}
496
+ ORDER BY timestamp DESC
497
+ LIMIT {limit}
498
+ """
499
+ params = (operation_type.value,)
500
+ else:
501
+ sql = f"""
502
+ SELECT * FROM audit_logs
503
+ ORDER BY timestamp DESC
504
+ LIMIT {limit}
505
+ """
506
+ params = ()
507
+
508
+ return self.db.execute(sql, params)
509
+ except Exception:
510
+ return []
511
+
512
+ def generate_rollback_sql(self, audit_id: int) -> Optional[str]:
513
+ """Generate rollback SQL for a given audit entry"""
514
+ audit_entry = self.get_by_id(audit_id)
515
+ if not audit_entry:
516
+ return None
517
+
518
+ operation_type = audit_entry['operation_type']
519
+ table_name = audit_entry['table_name']
520
+ record_id = audit_entry['record_id']
521
+
522
+ # For now, only handle simple cases
523
+ if operation_type == 'insert' and record_id:
524
+ # Rollback INSERT with DELETE
525
+ return f"DELETE FROM {table_name} WHERE id = {record_id}"
526
+
527
+ elif operation_type == 'delete' and record_id:
528
+ # Rollback DELETE is complex - would need to store original data
529
+ return f"-- Cannot auto-generate rollback for DELETE id={record_id} from {table_name}"
530
+
531
+ elif operation_type == 'update':
532
+ # Rollback UPDATE is complex - would need before/after values
533
+ return f"-- Cannot auto-generate rollback for UPDATE on {table_name}"
534
+
535
+ else:
536
+ return f"-- No rollback available for {operation_type} operation"
537
+
538
+ def execute_rollback(self, audit_id: int, dry_run: bool = True) -> Dict[str, Any]:
539
+ """Execute rollback for an audit entry"""
540
+ audit_entry = self.get_by_id(audit_id)
541
+ if not audit_entry:
542
+ return {'success': False, 'error': 'Audit entry not found'}
543
+
544
+ # Check if already rolled back
545
+ if audit_entry.get('rollback_status'):
546
+ return {'success': False, 'error': 'Operation already rolled back'}
547
+
548
+ # Generate rollback SQL
549
+ rollback_sql = self.generate_rollback_sql(audit_id)
550
+ if not rollback_sql or rollback_sql.startswith('--'):
551
+ return {'success': False, 'error': 'Cannot generate rollback SQL for this operation'}
552
+
553
+ if dry_run:
554
+ return {
555
+ 'success': True,
556
+ 'rollback_sql': rollback_sql,
557
+ 'dry_run': True,
558
+ 'audit_entry': audit_entry
559
+ }
560
+
561
+ try:
562
+ # Execute rollback
563
+ result = self.db.execute(rollback_sql)
564
+
565
+ # Update audit entry with rollback status
566
+ self.db.update('audit_logs',
567
+ {'rollback_sql': rollback_sql, 'rollback_status': 'completed'},
568
+ {'id': audit_id})
569
+
570
+ # Log the rollback operation itself
571
+ self.log_operation(
572
+ operation_type=AuditLogType.DELETE, # Or appropriate type
573
+ sql_query=rollback_sql,
574
+ status=AuditLogStatus.SUCCESS,
575
+ table_name=audit_entry['table_name'],
576
+ original_prompt=f"ROLLBACK of audit_id {audit_id}",
577
+ rows_affected=len(result) if isinstance(result, list) else 1
578
+ )
579
+
580
+ return {
581
+ 'success': True,
582
+ 'rollback_sql': rollback_sql,
583
+ 'result': result,
584
+ 'audit_entry': audit_entry
585
+ }
586
+
587
+ except Exception as e:
588
+ # Update audit entry with rollback error
589
+ self.db.update('audit_logs',
590
+ {'rollback_sql': rollback_sql, 'rollback_status': f'error: {str(e)}'},
591
+ {'id': audit_id})
592
+
593
+ return {
594
+ 'success': False,
595
+ 'error': str(e),
596
+ 'rollback_sql': rollback_sql,
597
+ 'audit_entry': audit_entry
598
+ }
599
+
600
+
601
+ # Convenience function
602
+ def create_audit_log(db_connection) -> AuditLog:
603
+ """Create an AuditLog instance"""
604
+ return AuditLog(db_connection)