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,438 @@
|
|
|
1
|
+
# createsonline/database.py
|
|
2
|
+
"""
|
|
3
|
+
CREATESONLINE Database Connection Module
|
|
4
|
+
|
|
5
|
+
Provides database connectivity for SQLite (built-in) and PostgreSQL (optional).
|
|
6
|
+
Core functionality has zero external dependencies.
|
|
7
|
+
|
|
8
|
+
Optional Dependencies:
|
|
9
|
+
- python-dotenv: For .env file support (pip install python-dotenv)
|
|
10
|
+
- psycopg2: For PostgreSQL support (pip install psycopg2-binary)
|
|
11
|
+
|
|
12
|
+
Without these, the module falls back to:
|
|
13
|
+
- System environment variables only (no .env)
|
|
14
|
+
- SQLite-only database support
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import json
|
|
19
|
+
import sqlite3
|
|
20
|
+
import hashlib
|
|
21
|
+
import logging
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from typing import Dict, Any, Optional, List
|
|
24
|
+
from enum import Enum
|
|
25
|
+
|
|
26
|
+
# Setup logging
|
|
27
|
+
logger = logging.getLogger("createsonline.database")
|
|
28
|
+
|
|
29
|
+
# Try to load dotenv if available (optional dependency)
|
|
30
|
+
try:
|
|
31
|
+
from dotenv import load_dotenv
|
|
32
|
+
load_dotenv()
|
|
33
|
+
logger.info("Environment variables loaded from .env file")
|
|
34
|
+
except ImportError:
|
|
35
|
+
logger.debug("dotenv not available - using system environment only")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ParamStyle(Enum):
|
|
39
|
+
"""SQL parameter styles for different databases"""
|
|
40
|
+
SQLITE = "?"
|
|
41
|
+
POSTGRESQL = "%s"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DatabaseConnection:
|
|
45
|
+
"""
|
|
46
|
+
Pure Python database connection handler for CREATESONLINE.
|
|
47
|
+
Supports SQLite (built-in) and PostgreSQL (via optional psycopg2).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
database_url: Optional[str] = None,
|
|
53
|
+
auto_create_tables: bool = True
|
|
54
|
+
):
|
|
55
|
+
self.database_url = database_url or os.getenv('DATABASE_URL', 'sqlite:///createsonline.db')
|
|
56
|
+
self.connection = None
|
|
57
|
+
self.db_type = self._detect_db_type()
|
|
58
|
+
self.param_style = ParamStyle.SQLITE if self.db_type == 'sqlite' else ParamStyle.POSTGRESQL
|
|
59
|
+
self.auto_create_tables = auto_create_tables
|
|
60
|
+
|
|
61
|
+
# Initialize connection
|
|
62
|
+
self._connect()
|
|
63
|
+
|
|
64
|
+
if self.auto_create_tables:
|
|
65
|
+
self._create_default_tables()
|
|
66
|
+
|
|
67
|
+
def _detect_db_type(self) -> str:
|
|
68
|
+
"""Detect database type from URL"""
|
|
69
|
+
if self.database_url.startswith('postgresql://'):
|
|
70
|
+
return 'postgresql'
|
|
71
|
+
elif self.database_url.startswith('sqlite://'):
|
|
72
|
+
return 'sqlite'
|
|
73
|
+
else:
|
|
74
|
+
return 'sqlite' # Default to SQLite
|
|
75
|
+
|
|
76
|
+
def _get_placeholder(self) -> str:
|
|
77
|
+
"""Get appropriate SQL placeholder for database type"""
|
|
78
|
+
return self.param_style.value
|
|
79
|
+
|
|
80
|
+
def _validate_identifier(self, identifier: str) -> str:
|
|
81
|
+
"""Validate and sanitize SQL identifiers (table/column names)"""
|
|
82
|
+
# Reserved SQL keywords to prevent injection and conflicts
|
|
83
|
+
reserved_words = {
|
|
84
|
+
'select', 'from', 'where', 'insert', 'update', 'delete', 'drop', 'create',
|
|
85
|
+
'table', 'database', 'index', 'alter', 'grant', 'revoke', 'commit', 'rollback',
|
|
86
|
+
'transaction', 'begin', 'end', 'union', 'join', 'inner', 'outer', 'left', 'right',
|
|
87
|
+
'group', 'order', 'having', 'distinct', 'count', 'sum', 'avg', 'max', 'min',
|
|
88
|
+
'and', 'or', 'not', 'null', 'true', 'false', 'is', 'like', 'in', 'exists'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Allow only alphanumeric characters, underscores, and dots
|
|
92
|
+
import re
|
|
93
|
+
if not re.match(r'^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)?$', identifier):
|
|
94
|
+
raise ValueError(f"Invalid SQL identifier: {identifier}")
|
|
95
|
+
|
|
96
|
+
# Check against reserved words (case-insensitive)
|
|
97
|
+
if identifier.lower() in reserved_words:
|
|
98
|
+
raise ValueError(f"SQL identifier '{identifier}' is a reserved word")
|
|
99
|
+
|
|
100
|
+
return identifier
|
|
101
|
+
|
|
102
|
+
def _connect(self):
|
|
103
|
+
"""Establish database connection"""
|
|
104
|
+
if self.db_type == 'sqlite':
|
|
105
|
+
# Extract SQLite path from URL
|
|
106
|
+
db_path = self.database_url.replace('sqlite:///', '').replace('sqlite://', '')
|
|
107
|
+
if db_path == ':memory:':
|
|
108
|
+
self.connection = sqlite3.connect(':memory:', check_same_thread=False)
|
|
109
|
+
else:
|
|
110
|
+
# Create directory if it doesn't exist
|
|
111
|
+
os.makedirs(os.path.dirname(db_path) if os.path.dirname(db_path) else '.', exist_ok=True)
|
|
112
|
+
self.connection = sqlite3.connect(db_path, check_same_thread=False)
|
|
113
|
+
|
|
114
|
+
# Enable row factory for dict-like access
|
|
115
|
+
self.connection.row_factory = sqlite3.Row
|
|
116
|
+
logger.info(f"Connected to SQLite database: {db_path}")
|
|
117
|
+
|
|
118
|
+
elif self.db_type == 'postgresql':
|
|
119
|
+
try:
|
|
120
|
+
import psycopg2
|
|
121
|
+
import psycopg2.extras
|
|
122
|
+
self.connection = psycopg2.connect(
|
|
123
|
+
self.database_url,
|
|
124
|
+
cursor_factory=psycopg2.extras.RealDictCursor
|
|
125
|
+
)
|
|
126
|
+
logger.info("Connected to PostgreSQL database")
|
|
127
|
+
except ImportError:
|
|
128
|
+
logger.warning("PostgreSQL support requires psycopg2. Falling back to SQLite.")
|
|
129
|
+
self.database_url = 'sqlite:///createsonline.db'
|
|
130
|
+
self.db_type = 'sqlite'
|
|
131
|
+
self.param_style = ParamStyle.SQLITE
|
|
132
|
+
return self._connect()
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Failed to connect to PostgreSQL: {e}")
|
|
135
|
+
raise
|
|
136
|
+
|
|
137
|
+
def _create_default_tables(self):
|
|
138
|
+
"""Create default CREATESONLINE framework tables"""
|
|
139
|
+
# Consolidated user table (removing duplicate users table)
|
|
140
|
+
user_table_sql = '''
|
|
141
|
+
CREATE TABLE IF NOT EXISTS createsonline_users (
|
|
142
|
+
id SERIAL PRIMARY KEY,
|
|
143
|
+
username VARCHAR(80) UNIQUE NOT NULL,
|
|
144
|
+
email VARCHAR(120) UNIQUE NOT NULL,
|
|
145
|
+
first_name VARCHAR(50),
|
|
146
|
+
last_name VARCHAR(50),
|
|
147
|
+
password_hash VARCHAR(128) NOT NULL,
|
|
148
|
+
is_active BOOLEAN DEFAULT TRUE,
|
|
149
|
+
is_staff BOOLEAN DEFAULT FALSE,
|
|
150
|
+
is_superuser BOOLEAN DEFAULT FALSE,
|
|
151
|
+
date_joined TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
152
|
+
last_login TIMESTAMP,
|
|
153
|
+
profile_picture TEXT,
|
|
154
|
+
bio TEXT,
|
|
155
|
+
failed_login_attempts INTEGER DEFAULT 0,
|
|
156
|
+
account_locked_until TIMESTAMP,
|
|
157
|
+
password_reset_token VARCHAR(128),
|
|
158
|
+
email_verification_token VARCHAR(128),
|
|
159
|
+
email_verified BOOLEAN DEFAULT FALSE
|
|
160
|
+
)
|
|
161
|
+
''' if self.db_type == 'postgresql' else '''
|
|
162
|
+
CREATE TABLE IF NOT EXISTS createsonline_users (
|
|
163
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
164
|
+
username VARCHAR(80) UNIQUE NOT NULL,
|
|
165
|
+
email VARCHAR(120) UNIQUE NOT NULL,
|
|
166
|
+
first_name VARCHAR(50),
|
|
167
|
+
last_name VARCHAR(50),
|
|
168
|
+
password_hash VARCHAR(128) NOT NULL,
|
|
169
|
+
is_active BOOLEAN DEFAULT TRUE,
|
|
170
|
+
is_staff BOOLEAN DEFAULT FALSE,
|
|
171
|
+
is_superuser BOOLEAN DEFAULT FALSE,
|
|
172
|
+
date_joined TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
173
|
+
last_login TIMESTAMP,
|
|
174
|
+
profile_picture TEXT,
|
|
175
|
+
bio TEXT,
|
|
176
|
+
failed_login_attempts INTEGER DEFAULT 0,
|
|
177
|
+
account_locked_until TIMESTAMP,
|
|
178
|
+
password_reset_token VARCHAR(128),
|
|
179
|
+
email_verification_token VARCHAR(128),
|
|
180
|
+
email_verified BOOLEAN DEFAULT FALSE
|
|
181
|
+
)
|
|
182
|
+
'''
|
|
183
|
+
|
|
184
|
+
tables = {
|
|
185
|
+
'createsonline_users': user_table_sql,
|
|
186
|
+
'ai_conversations': f'''
|
|
187
|
+
CREATE TABLE IF NOT EXISTS ai_conversations (
|
|
188
|
+
id {'SERIAL' if self.db_type == 'postgresql' else 'INTEGER'} PRIMARY KEY{' AUTOINCREMENT' if self.db_type == 'sqlite' else ''},
|
|
189
|
+
user_id INTEGER REFERENCES createsonline_users(id),
|
|
190
|
+
conversation_data TEXT NOT NULL,
|
|
191
|
+
ai_model VARCHAR(50) DEFAULT 'createsonline-internal',
|
|
192
|
+
tokens_used INTEGER DEFAULT 0,
|
|
193
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
194
|
+
)
|
|
195
|
+
''',
|
|
196
|
+
'app_settings': f'''
|
|
197
|
+
CREATE TABLE IF NOT EXISTS app_settings (
|
|
198
|
+
id {'SERIAL' if self.db_type == 'postgresql' else 'INTEGER'} PRIMARY KEY{' AUTOINCREMENT' if self.db_type == 'sqlite' else ''},
|
|
199
|
+
key VARCHAR(100) UNIQUE NOT NULL,
|
|
200
|
+
value TEXT NOT NULL,
|
|
201
|
+
description TEXT,
|
|
202
|
+
is_system BOOLEAN DEFAULT FALSE,
|
|
203
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
204
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
205
|
+
)
|
|
206
|
+
''',
|
|
207
|
+
'admin_sessions': f'''
|
|
208
|
+
CREATE TABLE IF NOT EXISTS admin_sessions (
|
|
209
|
+
id {'SERIAL' if self.db_type == 'postgresql' else 'INTEGER'} PRIMARY KEY{' AUTOINCREMENT' if self.db_type == 'sqlite' else ''},
|
|
210
|
+
user_id INTEGER REFERENCES createsonline_users(id),
|
|
211
|
+
session_token VARCHAR(128) UNIQUE NOT NULL,
|
|
212
|
+
expires_at TIMESTAMP NOT NULL,
|
|
213
|
+
ip_address VARCHAR(45),
|
|
214
|
+
user_agent TEXT,
|
|
215
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
216
|
+
)
|
|
217
|
+
'''
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for table_name, sql in tables.items():
|
|
221
|
+
try:
|
|
222
|
+
cursor = self.connection.cursor()
|
|
223
|
+
cursor.execute(sql)
|
|
224
|
+
self.connection.commit()
|
|
225
|
+
logger.info(f"Table '{table_name}' ready")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Error creating table '{table_name}': {e}")
|
|
228
|
+
raise
|
|
229
|
+
|
|
230
|
+
def execute(self, query: str, params: tuple = ()) -> List[Dict]:
|
|
231
|
+
"""Execute SQL query and return results"""
|
|
232
|
+
try:
|
|
233
|
+
cursor = self.connection.cursor()
|
|
234
|
+
cursor.execute(query, params)
|
|
235
|
+
|
|
236
|
+
if query.strip().upper().startswith('SELECT'):
|
|
237
|
+
if self.db_type == 'sqlite':
|
|
238
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
239
|
+
else:
|
|
240
|
+
return cursor.fetchall()
|
|
241
|
+
else:
|
|
242
|
+
self.connection.commit()
|
|
243
|
+
return [{"affected_rows": cursor.rowcount}]
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.exception(f"Database query failed: {query[:100]}...")
|
|
247
|
+
self.connection.rollback()
|
|
248
|
+
raise # Re-raise the exception instead of swallowing it
|
|
249
|
+
|
|
250
|
+
def insert(self, table: str, data: Dict[str, Any]) -> Optional[int]:
|
|
251
|
+
"""Insert data into table and return ID"""
|
|
252
|
+
safe_table = self._validate_identifier(table)
|
|
253
|
+
columns = ', '.join([self._validate_identifier(k) for k in data.keys()])
|
|
254
|
+
placeholders = ', '.join([self._get_placeholder()] * len(data))
|
|
255
|
+
|
|
256
|
+
if self.db_type == 'postgresql':
|
|
257
|
+
# PostgreSQL needs RETURNING for ID
|
|
258
|
+
query = f"INSERT INTO {safe_table} ({columns}) VALUES ({placeholders}) RETURNING id"
|
|
259
|
+
else:
|
|
260
|
+
query = f"INSERT INTO {safe_table} ({columns}) VALUES ({placeholders})"
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
cursor = self.connection.cursor()
|
|
264
|
+
cursor.execute(query, tuple(data.values()))
|
|
265
|
+
|
|
266
|
+
# Return last inserted ID
|
|
267
|
+
if self.db_type == 'sqlite':
|
|
268
|
+
row_id = cursor.lastrowid
|
|
269
|
+
self.connection.commit()
|
|
270
|
+
return row_id
|
|
271
|
+
else:
|
|
272
|
+
# PostgreSQL with RETURNING
|
|
273
|
+
row_id = cursor.fetchone()['id'] if cursor.rowcount > 0 else None
|
|
274
|
+
self.connection.commit()
|
|
275
|
+
return row_id
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error(f"Insert error: {e}")
|
|
279
|
+
self.connection.rollback()
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def update(self, table: str, data: Dict[str, Any], where: Dict[str, Any]) -> int:
|
|
283
|
+
"""Update data in table"""
|
|
284
|
+
safe_table = self._validate_identifier(table)
|
|
285
|
+
placeholder = self._get_placeholder()
|
|
286
|
+
set_clause = ', '.join([f"{self._validate_identifier(k)} = {placeholder}" for k in data.keys()])
|
|
287
|
+
where_clause = ' AND '.join([f"{self._validate_identifier(k)} = {placeholder}" for k in where.keys()])
|
|
288
|
+
|
|
289
|
+
query = f"UPDATE {safe_table} SET {set_clause} WHERE {where_clause}"
|
|
290
|
+
params = tuple(data.values()) + tuple(where.values())
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
cursor = self.connection.cursor()
|
|
294
|
+
cursor.execute(query, params)
|
|
295
|
+
self.connection.commit()
|
|
296
|
+
return cursor.rowcount
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.error(f"Update error: {e}")
|
|
299
|
+
self.connection.rollback()
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
def delete(self, table: str, where: Dict[str, Any]) -> int:
|
|
303
|
+
"""Delete data from table"""
|
|
304
|
+
safe_table = self._validate_identifier(table)
|
|
305
|
+
placeholder = self._get_placeholder()
|
|
306
|
+
where_clause = ' AND '.join([f"{self._validate_identifier(k)} = {placeholder}" for k in where.keys()])
|
|
307
|
+
query = f"DELETE FROM {safe_table} WHERE {where_clause}"
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
cursor = self.connection.cursor()
|
|
311
|
+
cursor.execute(query, tuple(where.values()))
|
|
312
|
+
self.connection.commit()
|
|
313
|
+
return cursor.rowcount
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error(f"Delete error: {e}")
|
|
316
|
+
self.connection.rollback()
|
|
317
|
+
return 0
|
|
318
|
+
|
|
319
|
+
def get_user_by_username(self, username: str) -> Optional[Dict]:
|
|
320
|
+
"""Get user by username from consolidated users table"""
|
|
321
|
+
placeholder = self._get_placeholder()
|
|
322
|
+
result = self.execute(f"SELECT * FROM createsonline_users WHERE username = {placeholder}", (username,))
|
|
323
|
+
return result[0] if result else None
|
|
324
|
+
|
|
325
|
+
def create_admin_user(self, username: str, email: str, password: str) -> Optional[int]:
|
|
326
|
+
"""Create admin user with hashed password"""
|
|
327
|
+
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
|
328
|
+
|
|
329
|
+
return self.insert('createsonline_users', {
|
|
330
|
+
'username': username,
|
|
331
|
+
'email': email,
|
|
332
|
+
'password_hash': password_hash,
|
|
333
|
+
'is_superuser': True,
|
|
334
|
+
'is_staff': True,
|
|
335
|
+
'is_active': True,
|
|
336
|
+
'email_verified': True
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
def create_session(self, user_id: int, ip_address: str = None, user_agent: str = None) -> str:
|
|
340
|
+
"""Create admin session and return token"""
|
|
341
|
+
import secrets
|
|
342
|
+
session_token = secrets.token_urlsafe(32)
|
|
343
|
+
|
|
344
|
+
# Session expires in 24 hours
|
|
345
|
+
from datetime import timedelta
|
|
346
|
+
expires_at = datetime.now() + timedelta(hours=24)
|
|
347
|
+
|
|
348
|
+
self.insert('admin_sessions', {
|
|
349
|
+
'user_id': user_id,
|
|
350
|
+
'session_token': session_token,
|
|
351
|
+
'expires_at': expires_at.isoformat(),
|
|
352
|
+
'ip_address': ip_address,
|
|
353
|
+
'user_agent': user_agent
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
return session_token
|
|
357
|
+
|
|
358
|
+
def validate_session(self, session_token: str) -> Optional[Dict]:
|
|
359
|
+
"""Validate session token and return user data"""
|
|
360
|
+
placeholder = self._get_placeholder()
|
|
361
|
+
query = f'''
|
|
362
|
+
SELECT u.*, s.expires_at
|
|
363
|
+
FROM createsonline_users u
|
|
364
|
+
JOIN admin_sessions s ON u.id = s.user_id
|
|
365
|
+
WHERE s.session_token = {placeholder} AND s.expires_at > CURRENT_TIMESTAMP
|
|
366
|
+
'''
|
|
367
|
+
result = self.execute(query, (session_token,))
|
|
368
|
+
return result[0] if result else None
|
|
369
|
+
|
|
370
|
+
def get_app_setting(self, key: str, default: Any = None) -> Any:
|
|
371
|
+
"""Get application setting"""
|
|
372
|
+
placeholder = self._get_placeholder()
|
|
373
|
+
result = self.execute(f"SELECT value FROM app_settings WHERE key = {placeholder}", (key,))
|
|
374
|
+
if result:
|
|
375
|
+
try:
|
|
376
|
+
return json.loads(result[0]['value'])
|
|
377
|
+
except:
|
|
378
|
+
return result[0]['value']
|
|
379
|
+
return default
|
|
380
|
+
|
|
381
|
+
def set_app_setting(self, key: str, value: Any, description: str = None):
|
|
382
|
+
"""Set application setting"""
|
|
383
|
+
# Check if setting exists
|
|
384
|
+
placeholder = self._get_placeholder()
|
|
385
|
+
existing = self.execute(f"SELECT id FROM app_settings WHERE key = {placeholder}", (key,))
|
|
386
|
+
|
|
387
|
+
value_str = json.dumps(value) if not isinstance(value, str) else value
|
|
388
|
+
|
|
389
|
+
if existing:
|
|
390
|
+
self.update('app_settings',
|
|
391
|
+
{'value': value_str, 'updated_at': datetime.now().isoformat()},
|
|
392
|
+
{'key': key})
|
|
393
|
+
else:
|
|
394
|
+
self.insert('app_settings', {
|
|
395
|
+
'key': key,
|
|
396
|
+
'value': value_str,
|
|
397
|
+
'description': description or f"Setting for {key}"
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
def verify_password(self, password: str, password_hash: str) -> bool:
|
|
401
|
+
"""Verify password against hash (uses consolidated SecurityUtils)"""
|
|
402
|
+
try:
|
|
403
|
+
from .utils import SecurityUtils
|
|
404
|
+
return SecurityUtils.verify_password(password, password_hash)
|
|
405
|
+
except ImportError:
|
|
406
|
+
# Fallback to simple verification for standalone use
|
|
407
|
+
import hashlib
|
|
408
|
+
import secrets
|
|
409
|
+
computed_hash = hashlib.sha256(password.encode()).hexdigest()
|
|
410
|
+
return secrets.compare_digest(computed_hash, password_hash)
|
|
411
|
+
|
|
412
|
+
def close(self):
|
|
413
|
+
"""Close database connection"""
|
|
414
|
+
if self.connection:
|
|
415
|
+
self.connection.close()
|
|
416
|
+
|
|
417
|
+
def __enter__(self):
|
|
418
|
+
return self
|
|
419
|
+
|
|
420
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
421
|
+
self.close()
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# Global database instance
|
|
425
|
+
_db_instance = None
|
|
426
|
+
|
|
427
|
+
def get_database() -> DatabaseConnection:
|
|
428
|
+
"""Get global database instance"""
|
|
429
|
+
global _db_instance
|
|
430
|
+
if _db_instance is None:
|
|
431
|
+
_db_instance = DatabaseConnection()
|
|
432
|
+
return _db_instance
|
|
433
|
+
|
|
434
|
+
def init_database(database_url: str = None) -> DatabaseConnection:
|
|
435
|
+
"""Initialize database with custom URL"""
|
|
436
|
+
global _db_instance
|
|
437
|
+
_db_instance = DatabaseConnection(database_url)
|
|
438
|
+
return _db_instance
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CREATESONLINE Internal HTTP Client Module
|
|
3
|
+
|
|
4
|
+
Pure Python HTTP client implementation with zero external dependencies.
|
|
5
|
+
Supports both synchronous and asynchronous operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .client import (
|
|
9
|
+
HTTPClient,
|
|
10
|
+
AsyncHTTPClient,
|
|
11
|
+
HTTPRequest,
|
|
12
|
+
HTTPResponse,
|
|
13
|
+
HTTPError,
|
|
14
|
+
ConnectionError,
|
|
15
|
+
TimeoutError,
|
|
16
|
+
RequestError
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
'HTTPClient',
|
|
21
|
+
'AsyncHTTPClient',
|
|
22
|
+
'HTTPRequest',
|
|
23
|
+
'HTTPResponse',
|
|
24
|
+
'HTTPError',
|
|
25
|
+
'ConnectionError',
|
|
26
|
+
'TimeoutError',
|
|
27
|
+
'RequestError'
|
|
28
|
+
]
|