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.
- createsonline/__init__.py +46 -0
- createsonline/admin/__init__.py +7 -0
- createsonline/admin/content.py +526 -0
- createsonline/admin/crud.py +805 -0
- createsonline/admin/field_builder.py +559 -0
- createsonline/admin/integration.py +482 -0
- createsonline/admin/interface.py +2562 -0
- createsonline/admin/model_creator.py +513 -0
- createsonline/admin/model_manager.py +388 -0
- createsonline/admin/modern_dashboard.py +498 -0
- createsonline/admin/permissions.py +264 -0
- createsonline/admin/user_forms.py +594 -0
- createsonline/ai/__init__.py +202 -0
- createsonline/ai/fields.py +1226 -0
- createsonline/ai/orm.py +325 -0
- createsonline/ai/services.py +1244 -0
- createsonline/app.py +506 -0
- createsonline/auth/__init__.py +8 -0
- createsonline/auth/management.py +228 -0
- createsonline/auth/models.py +552 -0
- createsonline/cli/__init__.py +5 -0
- createsonline/cli/commands/__init__.py +122 -0
- createsonline/cli/commands/database.py +416 -0
- createsonline/cli/commands/info.py +173 -0
- createsonline/cli/commands/initdb.py +218 -0
- createsonline/cli/commands/project.py +545 -0
- createsonline/cli/commands/serve.py +173 -0
- createsonline/cli/commands/shell.py +93 -0
- createsonline/cli/commands/users.py +148 -0
- createsonline/cli/main.py +2041 -0
- createsonline/cli/manage.py +274 -0
- createsonline/config/__init__.py +9 -0
- createsonline/config/app.py +2577 -0
- createsonline/config/database.py +179 -0
- createsonline/config/docs.py +384 -0
- createsonline/config/errors.py +160 -0
- createsonline/config/orm.py +43 -0
- createsonline/config/request.py +93 -0
- createsonline/config/settings.py +176 -0
- createsonline/data/__init__.py +23 -0
- createsonline/data/dataframe.py +925 -0
- createsonline/data/io.py +453 -0
- createsonline/data/series.py +557 -0
- createsonline/database/__init__.py +60 -0
- createsonline/database/abstraction.py +440 -0
- createsonline/database/assistant.py +585 -0
- createsonline/database/fields.py +442 -0
- createsonline/database/migrations.py +132 -0
- createsonline/database/models.py +604 -0
- createsonline/database.py +438 -0
- createsonline/http/__init__.py +28 -0
- createsonline/http/client.py +535 -0
- createsonline/ml/__init__.py +55 -0
- createsonline/ml/classification.py +552 -0
- createsonline/ml/clustering.py +680 -0
- createsonline/ml/metrics.py +542 -0
- createsonline/ml/neural.py +560 -0
- createsonline/ml/preprocessing.py +784 -0
- createsonline/ml/regression.py +501 -0
- createsonline/performance/__init__.py +19 -0
- createsonline/performance/cache.py +444 -0
- createsonline/performance/compression.py +335 -0
- createsonline/performance/core.py +419 -0
- createsonline/project_init.py +789 -0
- createsonline/routing.py +528 -0
- createsonline/security/__init__.py +34 -0
- createsonline/security/core.py +811 -0
- createsonline/security/encryption.py +349 -0
- createsonline/server.py +295 -0
- createsonline/static/css/admin.css +263 -0
- createsonline/static/css/common.css +358 -0
- createsonline/static/css/dashboard.css +89 -0
- createsonline/static/favicon.ico +0 -0
- createsonline/static/icons/icon-128x128.png +0 -0
- createsonline/static/icons/icon-128x128.webp +0 -0
- createsonline/static/icons/icon-16x16.png +0 -0
- createsonline/static/icons/icon-16x16.webp +0 -0
- createsonline/static/icons/icon-180x180.png +0 -0
- createsonline/static/icons/icon-180x180.webp +0 -0
- createsonline/static/icons/icon-192x192.png +0 -0
- createsonline/static/icons/icon-192x192.webp +0 -0
- createsonline/static/icons/icon-256x256.png +0 -0
- createsonline/static/icons/icon-256x256.webp +0 -0
- createsonline/static/icons/icon-32x32.png +0 -0
- createsonline/static/icons/icon-32x32.webp +0 -0
- createsonline/static/icons/icon-384x384.png +0 -0
- createsonline/static/icons/icon-384x384.webp +0 -0
- createsonline/static/icons/icon-48x48.png +0 -0
- createsonline/static/icons/icon-48x48.webp +0 -0
- createsonline/static/icons/icon-512x512.png +0 -0
- createsonline/static/icons/icon-512x512.webp +0 -0
- createsonline/static/icons/icon-64x64.png +0 -0
- createsonline/static/icons/icon-64x64.webp +0 -0
- createsonline/static/image/android-chrome-192x192.png +0 -0
- createsonline/static/image/android-chrome-512x512.png +0 -0
- createsonline/static/image/apple-touch-icon.png +0 -0
- createsonline/static/image/favicon-16x16.png +0 -0
- createsonline/static/image/favicon-32x32.png +0 -0
- createsonline/static/image/favicon.ico +0 -0
- createsonline/static/image/favicon.svg +17 -0
- createsonline/static/image/icon-128x128.png +0 -0
- createsonline/static/image/icon-128x128.webp +0 -0
- createsonline/static/image/icon-16x16.png +0 -0
- createsonline/static/image/icon-16x16.webp +0 -0
- createsonline/static/image/icon-180x180.png +0 -0
- createsonline/static/image/icon-180x180.webp +0 -0
- createsonline/static/image/icon-192x192.png +0 -0
- createsonline/static/image/icon-192x192.webp +0 -0
- createsonline/static/image/icon-256x256.png +0 -0
- createsonline/static/image/icon-256x256.webp +0 -0
- createsonline/static/image/icon-32x32.png +0 -0
- createsonline/static/image/icon-32x32.webp +0 -0
- createsonline/static/image/icon-384x384.png +0 -0
- createsonline/static/image/icon-384x384.webp +0 -0
- createsonline/static/image/icon-48x48.png +0 -0
- createsonline/static/image/icon-48x48.webp +0 -0
- createsonline/static/image/icon-512x512.png +0 -0
- createsonline/static/image/icon-512x512.webp +0 -0
- createsonline/static/image/icon-64x64.png +0 -0
- createsonline/static/image/icon-64x64.webp +0 -0
- createsonline/static/image/logo-header-h100.png +0 -0
- createsonline/static/image/logo-header-h100.webp +0 -0
- createsonline/static/image/logo-header-h200@2x.png +0 -0
- createsonline/static/image/logo-header-h200@2x.webp +0 -0
- createsonline/static/image/logo.png +0 -0
- createsonline/static/js/admin.js +274 -0
- createsonline/static/site.webmanifest +35 -0
- createsonline/static/templates/admin/base.html +87 -0
- createsonline/static/templates/admin/dashboard.html +217 -0
- createsonline/static/templates/admin/model_form.html +270 -0
- createsonline/static/templates/admin/model_list.html +202 -0
- createsonline/static/test_script.js +15 -0
- createsonline/static/test_styles.css +59 -0
- createsonline/static_files.py +365 -0
- createsonline/templates/404.html +100 -0
- createsonline/templates/admin_login.html +169 -0
- createsonline/templates/base.html +102 -0
- createsonline/templates/index.html +151 -0
- createsonline/templates.py +205 -0
- createsonline/testing.py +322 -0
- createsonline/utils.py +448 -0
- createsonline/validation/__init__.py +49 -0
- createsonline/validation/fields.py +598 -0
- createsonline/validation/models.py +504 -0
- createsonline/validation/validators.py +561 -0
- createsonline/views.py +184 -0
- createsonline-0.1.26.dist-info/METADATA +46 -0
- createsonline-0.1.26.dist-info/RECORD +152 -0
- createsonline-0.1.26.dist-info/WHEEL +5 -0
- createsonline-0.1.26.dist-info/entry_points.txt +2 -0
- createsonline-0.1.26.dist-info/licenses/LICENSE +21 -0
- 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)
|