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,442 @@
1
+ """
2
+ CREATESONLINE Database Fields
3
+ Field types that integrate with AI functionality.
4
+ """
5
+ from typing import Any, Optional, Union, Dict, Callable
6
+ from datetime import datetime, date
7
+ from sqlalchemy import Column, Integer, String, Text, Float, Boolean, DateTime, Date, JSON
8
+ from sqlalchemy.types import TypeDecorator, VARCHAR
9
+ import json
10
+
11
+ # Base field class for compatibility
12
+ class CreatesonlineField:
13
+ """
14
+ Base CREATESONLINE field class for database abstraction.
15
+ Provides compatibility layer for the Pure Independence implementation.
16
+ """
17
+
18
+ def __init__(self,
19
+ primary_key: bool = False,
20
+ nullable: bool = True,
21
+ default: Any = None,
22
+ unique: bool = False,
23
+ index: bool = False,
24
+ **kwargs):
25
+ self.primary_key = primary_key
26
+ self.nullable = nullable
27
+ self.default = default
28
+ self.unique = unique
29
+ self.index = index
30
+ self.extra_kwargs = kwargs
31
+
32
+ def to_column(self, name: str) -> Column:
33
+ """Convert to SQLAlchemy column - must be implemented by subclasses."""
34
+ raise NotImplementedError("Subclasses must implement to_column method")
35
+
36
+
37
+ class AIEnhancedField(CreatesonlineField):
38
+ """
39
+ Base class for AI-enhanced fields that can be processed by AI models.
40
+ """
41
+
42
+ def __init__(self,
43
+ ai_processable: bool = False,
44
+ ai_embedding: bool = False,
45
+ ai_searchable: bool = False,
46
+ ai_summarizable: bool = False,
47
+ **kwargs):
48
+ super().__init__(**kwargs)
49
+ self.ai_processable = ai_processable
50
+ self.ai_embedding = ai_embedding
51
+ self.ai_searchable = ai_searchable
52
+ self.ai_summarizable = ai_summarizable
53
+ self.ai_metadata = {k: v for k, v in kwargs.items() if k.startswith('ai_')}
54
+
55
+
56
+ class StringField(AIEnhancedField):
57
+ """Enhanced string field with AI capabilities."""
58
+
59
+ def __init__(self,
60
+ max_length: int = 255,
61
+ nullable: bool = True,
62
+ default: Optional[str] = None,
63
+ unique: bool = False,
64
+ index: bool = False,
65
+ **ai_kwargs):
66
+ super().__init__(**ai_kwargs)
67
+ self.max_length = max_length
68
+ self.nullable = nullable
69
+ self.default = default
70
+ self.unique = unique
71
+ self.index = index
72
+
73
+ def to_column(self, name: str) -> Column:
74
+ """Convert to SQLAlchemy column."""
75
+ return Column(
76
+ name,
77
+ String(self.max_length),
78
+ nullable=self.nullable,
79
+ default=self.default,
80
+ unique=self.unique,
81
+ index=self.index
82
+ )
83
+
84
+
85
+ class TextField(AIEnhancedField):
86
+ """Enhanced text field for long content with AI capabilities."""
87
+
88
+ def __init__(self,
89
+ nullable: bool = True,
90
+ default: Optional[str] = None,
91
+ **ai_kwargs):
92
+ super().__init__(**ai_kwargs)
93
+ self.nullable = nullable
94
+ self.default = default
95
+
96
+ def to_column(self, name: str) -> Column:
97
+ """Convert to SQLAlchemy column."""
98
+ return Column(
99
+ name,
100
+ Text,
101
+ nullable=self.nullable,
102
+ default=self.default
103
+ )
104
+
105
+
106
+ class IntegerField(AIEnhancedField):
107
+ """Enhanced integer field with AI capabilities."""
108
+
109
+ def __init__(self,
110
+ nullable: bool = True,
111
+ default: Optional[int] = None,
112
+ unique: bool = False,
113
+ index: bool = False,
114
+ primary_key: bool = False,
115
+ autoincrement: bool = False,
116
+ **ai_kwargs):
117
+ super().__init__(**ai_kwargs)
118
+ self.nullable = nullable
119
+ self.default = default
120
+ self.unique = unique
121
+ self.index = index
122
+ self.primary_key = primary_key
123
+ self.autoincrement = autoincrement
124
+
125
+ def to_column(self, name: str) -> Column:
126
+ """Convert to SQLAlchemy column."""
127
+ return Column(
128
+ name,
129
+ Integer,
130
+ nullable=self.nullable,
131
+ default=self.default,
132
+ unique=self.unique,
133
+ index=self.index,
134
+ primary_key=self.primary_key,
135
+ autoincrement=self.autoincrement
136
+ )
137
+
138
+
139
+ class FloatField(AIEnhancedField):
140
+ """Enhanced float field with AI capabilities."""
141
+
142
+ def __init__(self,
143
+ nullable: bool = True,
144
+ default: Optional[float] = None,
145
+ unique: bool = False,
146
+ index: bool = False,
147
+ **ai_kwargs):
148
+ super().__init__(**ai_kwargs)
149
+ self.nullable = nullable
150
+ self.default = default
151
+ self.unique = unique
152
+ self.index = index
153
+
154
+ def to_column(self, name: str) -> Column:
155
+ """Convert to SQLAlchemy column."""
156
+ return Column(
157
+ name,
158
+ Float,
159
+ nullable=self.nullable,
160
+ default=self.default,
161
+ unique=self.unique,
162
+ index=self.index
163
+ )
164
+
165
+
166
+ class BooleanField(AIEnhancedField):
167
+ """Enhanced boolean field with AI capabilities."""
168
+
169
+ def __init__(self,
170
+ default: Optional[bool] = None,
171
+ nullable: bool = True,
172
+ **ai_kwargs):
173
+ super().__init__(**ai_kwargs)
174
+ self.default = default
175
+ self.nullable = nullable
176
+
177
+ def to_column(self, name: str) -> Column:
178
+ """Convert to SQLAlchemy column."""
179
+ return Column(
180
+ name,
181
+ Boolean,
182
+ default=self.default,
183
+ nullable=self.nullable
184
+ )
185
+
186
+
187
+ class DateTimeField(AIEnhancedField):
188
+ """Enhanced datetime field with AI capabilities."""
189
+
190
+ def __init__(self,
191
+ auto_now: bool = False,
192
+ auto_now_add: bool = False,
193
+ default: Optional[Union[datetime, Callable]] = None,
194
+ nullable: bool = True,
195
+ **ai_kwargs):
196
+ super().__init__(**ai_kwargs)
197
+ self.auto_now = auto_now
198
+ self.auto_now_add = auto_now_add
199
+ self.nullable = nullable
200
+
201
+ if auto_now_add and default is None:
202
+ self.default = datetime.utcnow
203
+ elif auto_now and default is None:
204
+ self.default = datetime.utcnow
205
+ else:
206
+ self.default = default
207
+
208
+ def to_column(self, name: str) -> Column:
209
+ """Convert to SQLAlchemy column."""
210
+ onupdate = datetime.utcnow if self.auto_now else None
211
+
212
+ return Column(
213
+ name,
214
+ DateTime,
215
+ default=self.default,
216
+ onupdate=onupdate,
217
+ nullable=self.nullable
218
+ )
219
+
220
+
221
+ class DateField(AIEnhancedField):
222
+ """Enhanced date field with AI capabilities."""
223
+
224
+ def __init__(self,
225
+ auto_now: bool = False,
226
+ auto_now_add: bool = False,
227
+ default: Optional[Union[date, Callable]] = None,
228
+ nullable: bool = True,
229
+ **ai_kwargs):
230
+ super().__init__(**ai_kwargs)
231
+ self.auto_now = auto_now
232
+ self.auto_now_add = auto_now_add
233
+ self.nullable = nullable
234
+
235
+ if auto_now_add and default is None:
236
+ self.default = lambda: datetime.utcnow().date()
237
+ elif auto_now and default is None:
238
+ self.default = lambda: datetime.utcnow().date()
239
+ else:
240
+ self.default = default
241
+
242
+ def to_column(self, name: str) -> Column:
243
+ """Convert to SQLAlchemy column."""
244
+ onupdate = lambda: datetime.utcnow().date() if self.auto_now else None
245
+
246
+ return Column(
247
+ name,
248
+ Date,
249
+ default=self.default,
250
+ onupdate=onupdate,
251
+ nullable=self.nullable
252
+ )
253
+
254
+
255
+ class JSONField(TypeDecorator, AIEnhancedField):
256
+ """Enhanced JSON field with AI capabilities."""
257
+
258
+ impl = VARCHAR
259
+ cache_ok = True
260
+
261
+ def __init__(self,
262
+ nullable: bool = True,
263
+ default: Optional[Union[dict, list]] = None,
264
+ **ai_kwargs):
265
+ # Call parent constructors properly
266
+ TypeDecorator.__init__(self)
267
+ AIEnhancedField.__init__(self, **ai_kwargs)
268
+ self.nullable = nullable
269
+ self.default = default or {}
270
+
271
+ def load_dialect_impl(self, dialect):
272
+ """Load appropriate dialect implementation."""
273
+ if dialect.name == 'postgresql':
274
+ return dialect.type_descriptor(JSON())
275
+ else:
276
+ return dialect.type_descriptor(VARCHAR())
277
+
278
+ def process_bind_param(self, value, dialect):
279
+ """Process value when binding to database."""
280
+ if value is not None:
281
+ return json.dumps(value)
282
+ return value
283
+
284
+ def process_result_value(self, value, dialect):
285
+ """Process value when retrieving from database."""
286
+ if value is not None:
287
+ try:
288
+ return json.loads(value)
289
+ except (ValueError, TypeError):
290
+ return value
291
+ return value
292
+
293
+ def to_column(self, name: str) -> Column:
294
+ """Convert to SQLAlchemy column."""
295
+ return Column(
296
+ name,
297
+ self,
298
+ nullable=self.nullable,
299
+ default=self.default
300
+ )
301
+
302
+
303
+ class EmbeddingField(AIEnhancedField):
304
+ """Special field for storing AI embeddings."""
305
+
306
+ def __init__(self,
307
+ dimensions: int = 1536, # OpenAI default
308
+ nullable: bool = True,
309
+ **ai_kwargs):
310
+ super().__init__(ai_embedding=True, **ai_kwargs)
311
+ self.dimensions = dimensions
312
+ self.nullable = nullable
313
+
314
+ def to_column(self, name: str) -> Column:
315
+ """Convert to SQLAlchemy column (stores as JSON)."""
316
+ return Column(
317
+ name,
318
+ JSON,
319
+ nullable=self.nullable
320
+ )
321
+
322
+
323
+ class SlugField(StringField):
324
+ """URL-friendly slug field."""
325
+
326
+ def __init__(self,
327
+ max_length: int = 100,
328
+ unique: bool = True,
329
+ index: bool = True,
330
+ **kwargs):
331
+ super().__init__(
332
+ max_length=max_length,
333
+ unique=unique,
334
+ index=index,
335
+ **kwargs
336
+ )
337
+
338
+
339
+ class EmailField(StringField):
340
+ """Email field with validation."""
341
+
342
+ def __init__(self,
343
+ max_length: int = 254, # RFC 5321
344
+ unique: bool = False,
345
+ index: bool = True,
346
+ **kwargs):
347
+ super().__init__(
348
+ max_length=max_length,
349
+ unique=unique,
350
+ index=index,
351
+ **kwargs
352
+ )
353
+
354
+
355
+ class URLField(StringField):
356
+ """URL field."""
357
+
358
+ def __init__(self,
359
+ max_length: int = 2048,
360
+ **kwargs):
361
+ super().__init__(
362
+ max_length=max_length,
363
+ **kwargs
364
+ )
365
+
366
+
367
+ # Convenience functions for creating columns
368
+ def create_string_column(name: str, field: StringField) -> Column:
369
+ """Create string column from field."""
370
+ return field.to_column(name)
371
+
372
+ def create_text_column(name: str, field: TextField) -> Column:
373
+ """Create text column from field."""
374
+ return field.to_column(name)
375
+
376
+ def create_integer_column(name: str, field: IntegerField) -> Column:
377
+ """Create integer column from field."""
378
+ return field.to_column(name)
379
+
380
+ def create_float_column(name: str, field: FloatField) -> Column:
381
+ """Create float column from field."""
382
+ return field.to_column(name)
383
+
384
+ def create_boolean_column(name: str, field: BooleanField) -> Column:
385
+ """Create boolean column from field."""
386
+ return field.to_column(name)
387
+
388
+ def create_datetime_column(name: str, field: DateTimeField) -> Column:
389
+ """Create datetime column from field."""
390
+ return field.to_column(name)
391
+
392
+ def create_date_column(name: str, field: DateField) -> Column:
393
+ """Create date column from field."""
394
+ return field.to_column(name)
395
+
396
+ def create_json_column(name: str, field: JSONField) -> Column:
397
+ """Create JSON column from field."""
398
+ return field.to_column(name)
399
+
400
+ def create_embedding_column(name: str, field: EmbeddingField) -> Column:
401
+ """Create embedding column from field."""
402
+ return field.to_column(name)
403
+
404
+
405
+ # Field registry for AI processing
406
+ AI_FIELD_REGISTRY = {}
407
+
408
+ def register_ai_field(model_class, field_name: str, field: AIEnhancedField):
409
+ """Register a field for AI processing."""
410
+ model_name = model_class.__name__
411
+ if model_name not in AI_FIELD_REGISTRY:
412
+ AI_FIELD_REGISTRY[model_name] = {}
413
+
414
+ AI_FIELD_REGISTRY[model_name][field_name] = {
415
+ 'field': field,
416
+ 'ai_processable': field.ai_processable,
417
+ 'ai_embedding': field.ai_embedding,
418
+ 'ai_searchable': field.ai_searchable,
419
+ 'ai_summarizable': field.ai_summarizable,
420
+ 'ai_metadata': field.ai_metadata
421
+ }
422
+
423
+ def get_ai_fields(model_class) -> Dict[str, Dict]:
424
+ """Get all AI-enhanced fields for a model."""
425
+ model_name = model_class.__name__
426
+ return AI_FIELD_REGISTRY.get(model_name, {})
427
+
428
+ def get_embedding_fields(model_class) -> Dict[str, Dict]:
429
+ """Get all embedding fields for a model."""
430
+ ai_fields = get_ai_fields(model_class)
431
+ return {
432
+ name: info for name, info in ai_fields.items()
433
+ if info.get('ai_embedding', False)
434
+ }
435
+
436
+ def get_searchable_fields(model_class) -> Dict[str, Dict]:
437
+ """Get all searchable fields for a model."""
438
+ ai_fields = get_ai_fields(model_class)
439
+ return {
440
+ name: info for name, info in ai_fields.items()
441
+ if info.get('ai_searchable', False)
442
+ }
@@ -0,0 +1,132 @@
1
+ """
2
+ CREATESONLINE Database Migrations
3
+ Simple database migration system for Pure Independence architecture.
4
+ """
5
+
6
+ from typing import List, Dict, Any
7
+ from datetime import datetime
8
+ import json
9
+ import logging
10
+ from pathlib import Path
11
+
12
+ # Setup logger
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class Migration:
16
+ """
17
+ Represents a single database migration.
18
+ """
19
+
20
+ def __init__(self, name: str, description: str = "", version: str = ""):
21
+ self.name = name
22
+ self.description = description
23
+ self.version = version
24
+ self.timestamp = datetime.now()
25
+ self.applied = False
26
+
27
+ def up(self):
28
+ """Apply this migration"""
29
+ raise NotImplementedError("Subclasses must implement up() method")
30
+
31
+ def down(self):
32
+ """Reverse this migration"""
33
+ raise NotImplementedError("Subclasses must implement down() method")
34
+
35
+
36
+ class MigrationManager:
37
+ """
38
+ Manages database migrations for CREATESONLINE Pure Independence.
39
+ """
40
+
41
+ def __init__(self, migrations_dir: str = "migrations"):
42
+ self.migrations_dir = Path(migrations_dir)
43
+ self.migrations_dir.mkdir(exist_ok=True)
44
+ self.applied_migrations_file = self.migrations_dir / "applied.json"
45
+ self.migrations: List[Migration] = []
46
+ self.applied_migrations = self._load_applied_migrations()
47
+
48
+ def _load_applied_migrations(self) -> List[str]:
49
+ """Load list of applied migrations"""
50
+ if self.applied_migrations_file.exists():
51
+ try:
52
+ with open(self.applied_migrations_file, 'r') as f:
53
+ return json.load(f)
54
+ except (json.JSONDecodeError, IOError):
55
+ return []
56
+ return []
57
+
58
+ def _save_applied_migrations(self):
59
+ """Save list of applied migrations"""
60
+ try:
61
+ with open(self.applied_migrations_file, 'w') as f:
62
+ json.dump(self.applied_migrations, f, indent=2)
63
+ except IOError as e:
64
+ logger.warning(f"Could not save applied migrations: {e}")
65
+
66
+ def add_migration(self, migration: Migration):
67
+ """Add a migration to the manager"""
68
+ self.migrations.append(migration)
69
+
70
+ def apply_migrations(self):
71
+ """Apply all pending migrations"""
72
+ applied_count = 0
73
+ pending_migrations = [m for m in self.migrations if m.name not in self.applied_migrations]
74
+
75
+ if not pending_migrations:
76
+ logger.info("No pending migrations to apply")
77
+ return
78
+
79
+ logger.info(f"Applying {len(pending_migrations)} pending migrations...")
80
+
81
+ for migration in pending_migrations:
82
+ logger.info(f"Applying migration: {migration.name}")
83
+ try:
84
+ migration.up()
85
+ self.applied_migrations.append(migration.name)
86
+ migration.applied = True
87
+ applied_count += 1
88
+ logger.info(f"✅ Applied: {migration.name}")
89
+ except Exception as e:
90
+ logger.error(f"❌ Failed to apply {migration.name}: {e}")
91
+ break
92
+
93
+ if applied_count > 0:
94
+ self._save_applied_migrations()
95
+ logger.info(f"Applied {applied_count} migrations successfully")
96
+
97
+ def rollback_migration(self, migration_name: str):
98
+ """Rollback a specific migration"""
99
+ for migration in reversed(self.migrations):
100
+ if migration.name == migration_name:
101
+ if migration_name in self.applied_migrations:
102
+ logger.info(f"Rolling back migration: {migration_name}")
103
+ try:
104
+ migration.down()
105
+ self.applied_migrations.remove(migration_name)
106
+ migration.applied = False
107
+ self._save_applied_migrations()
108
+ logger.info(f"✅ Rolled back: {migration_name}")
109
+ except Exception as e:
110
+ logger.error(f"❌ Failed to rollback {migration_name}: {e}")
111
+ else:
112
+ logger.warning(f"Migration {migration_name} is not applied")
113
+ return
114
+
115
+ logger.error(f"Migration {migration_name} not found")
116
+
117
+ def get_migration_status(self) -> Dict[str, Any]:
118
+ """Get status of all migrations"""
119
+ return {
120
+ "total_migrations": len(self.migrations),
121
+ "applied_migrations": len(self.applied_migrations),
122
+ "pending_migrations": len(self.migrations) - len(self.applied_migrations),
123
+ "migrations": [
124
+ {
125
+ "name": m.name,
126
+ "description": m.description,
127
+ "applied": m.name in self.applied_migrations,
128
+ "timestamp": m.timestamp.isoformat() if hasattr(m, 'timestamp') else None
129
+ }
130
+ for m in self.migrations
131
+ ]
132
+ }