Spaces:
Building
Building
Update config/config_provider.py
Browse files- config/config_provider.py +949 -949
config/config_provider.py
CHANGED
@@ -1,950 +1,950 @@
|
|
1 |
-
"""
|
2 |
-
Thread-Safe Configuration Provider for Flare Platform
|
3 |
-
"""
|
4 |
-
import threading
|
5 |
-
import os
|
6 |
-
import json
|
7 |
-
import commentjson
|
8 |
-
from typing import Optional, Dict, List, Any
|
9 |
-
from datetime import datetime
|
10 |
-
from pathlib import Path
|
11 |
-
import tempfile
|
12 |
-
import shutil
|
13 |
-
from utils import get_current_timestamp, normalize_timestamp, timestamps_equal
|
14 |
-
|
15 |
-
from config_models import (
|
16 |
-
ServiceConfig, GlobalConfig, ProjectConfig, VersionConfig,
|
17 |
-
IntentConfig, APIConfig, ActivityLogEntry, ParameterConfig,
|
18 |
-
LLMConfiguration, GenerationConfig
|
19 |
-
)
|
20 |
-
from utils.logger import log_info, log_error, log_warning, log_debug, LogTimer
|
21 |
-
from utils.exceptions import (
|
22 |
-
RaceConditionError, ConfigurationError, ResourceNotFoundError,
|
23 |
-
DuplicateResourceError, ValidationError
|
24 |
-
)
|
25 |
-
from utils.encryption_utils import encrypt, decrypt
|
26 |
-
|
27 |
-
class ConfigProvider:
|
28 |
-
"""Thread-safe singleton configuration provider"""
|
29 |
-
|
30 |
-
_instance: Optional[ServiceConfig] = None
|
31 |
-
_lock = threading.RLock() # Reentrant lock for nested calls
|
32 |
-
_file_lock = threading.Lock() # Separate lock for file operations
|
33 |
-
_CONFIG_PATH = Path(__file__).parent.parent / "service_config.jsonc"
|
34 |
-
|
35 |
-
@staticmethod
|
36 |
-
def _normalize_date(date_str: Optional[str]) -> str:
|
37 |
-
"""Normalize date string for comparison"""
|
38 |
-
if not date_str:
|
39 |
-
return ""
|
40 |
-
return date_str.replace(' ', 'T').replace('+00:00', 'Z').replace('.000Z', 'Z')
|
41 |
-
|
42 |
-
@classmethod
|
43 |
-
def get(cls) -> ServiceConfig:
|
44 |
-
"""Get cached configuration - thread-safe"""
|
45 |
-
if cls._instance is None:
|
46 |
-
with cls._lock:
|
47 |
-
# Double-checked locking pattern
|
48 |
-
if cls._instance is None:
|
49 |
-
with LogTimer("config_load"):
|
50 |
-
cls._instance = cls._load()
|
51 |
-
cls._instance.build_index()
|
52 |
-
log_info("Configuration loaded successfully")
|
53 |
-
return cls._instance
|
54 |
-
|
55 |
-
@classmethod
|
56 |
-
def reload(cls) -> ServiceConfig:
|
57 |
-
"""Force reload configuration from file"""
|
58 |
-
with cls._lock:
|
59 |
-
log_info("Reloading configuration...")
|
60 |
-
cls._instance = None
|
61 |
-
return cls.get()
|
62 |
-
|
63 |
-
@classmethod
|
64 |
-
def _load(cls) -> ServiceConfig:
|
65 |
-
"""Load configuration from file"""
|
66 |
-
try:
|
67 |
-
if not cls._CONFIG_PATH.exists():
|
68 |
-
raise ConfigurationError(
|
69 |
-
f"Config file not found: {cls._CONFIG_PATH}",
|
70 |
-
config_key="service_config.jsonc"
|
71 |
-
)
|
72 |
-
|
73 |
-
with open(cls._CONFIG_PATH, 'r', encoding='utf-8') as f:
|
74 |
-
config_data = commentjson.load(f)
|
75 |
-
|
76 |
-
# Debug: İlk project'in tarihini kontrol et
|
77 |
-
if 'projects' in config_data and len(config_data['projects']) > 0:
|
78 |
-
first_project = config_data['projects'][0]
|
79 |
-
log_debug(f"🔍 Raw project data - last_update_date: {first_project.get('last_update_date')}")
|
80 |
-
|
81 |
-
# Ensure required fields
|
82 |
-
if 'config' not in config_data:
|
83 |
-
config_data['config'] = {}
|
84 |
-
|
85 |
-
# Ensure providers exist
|
86 |
-
cls._ensure_providers(config_data)
|
87 |
-
|
88 |
-
# Parse API configs (handle JSON strings)
|
89 |
-
if 'apis' in config_data:
|
90 |
-
cls._parse_api_configs(config_data['apis'])
|
91 |
-
|
92 |
-
# Validate and create model
|
93 |
-
cfg = ServiceConfig.model_validate(config_data)
|
94 |
-
|
95 |
-
# Debug: Model'e dönüştükten sonra kontrol et
|
96 |
-
if cfg.projects and len(cfg.projects) > 0:
|
97 |
-
log_debug(f"🔍 Parsed project - last_update_date: {cfg.projects[0].last_update_date}")
|
98 |
-
log_debug(f"🔍 Type: {type(cfg.projects[0].last_update_date)}")
|
99 |
-
|
100 |
-
# Log versions published status after parsing
|
101 |
-
for version in cfg.projects[0].versions:
|
102 |
-
log_debug(f"🔍 Parsed version {version.no} - published: {version.published} (type: {type(version.published)})")
|
103 |
-
|
104 |
-
log_debug(
|
105 |
-
"Configuration loaded",
|
106 |
-
projects=len(cfg.projects),
|
107 |
-
apis=len(cfg.apis),
|
108 |
-
users=len(cfg.global_config.users)
|
109 |
-
)
|
110 |
-
|
111 |
-
return cfg
|
112 |
-
|
113 |
-
except Exception as e:
|
114 |
-
log_error(f"Error loading config", error=str(e), path=str(cls._CONFIG_PATH))
|
115 |
-
raise ConfigurationError(f"Failed to load configuration: {e}")
|
116 |
-
|
117 |
-
@classmethod
|
118 |
-
def _parse_api_configs(cls, apis: List[Dict[str, Any]]) -> None:
|
119 |
-
"""Parse JSON string fields in API configs"""
|
120 |
-
for api in apis:
|
121 |
-
# Parse headers
|
122 |
-
if 'headers' in api and isinstance(api['headers'], str):
|
123 |
-
try:
|
124 |
-
api['headers'] = json.loads(api['headers'])
|
125 |
-
except json.JSONDecodeError:
|
126 |
-
api['headers'] = {}
|
127 |
-
|
128 |
-
# Parse body_template
|
129 |
-
if 'body_template' in api and isinstance(api['body_template'], str):
|
130 |
-
try:
|
131 |
-
api['body_template'] = json.loads(api['body_template'])
|
132 |
-
except json.JSONDecodeError:
|
133 |
-
api['body_template'] = {}
|
134 |
-
|
135 |
-
# Parse auth configs
|
136 |
-
if 'auth' in api and api['auth']:
|
137 |
-
cls._parse_auth_config(api['auth'])
|
138 |
-
|
139 |
-
@classmethod
|
140 |
-
def _parse_auth_config(cls, auth: Dict[str, Any]) -> None:
|
141 |
-
"""Parse auth configuration"""
|
142 |
-
# Parse token_request_body
|
143 |
-
if 'token_request_body' in auth and isinstance(auth['token_request_body'], str):
|
144 |
-
try:
|
145 |
-
auth['token_request_body'] = json.loads(auth['token_request_body'])
|
146 |
-
except json.JSONDecodeError:
|
147 |
-
auth['token_request_body'] = {}
|
148 |
-
|
149 |
-
# Parse token_refresh_body
|
150 |
-
if 'token_refresh_body' in auth and isinstance(auth['token_refresh_body'], str):
|
151 |
-
try:
|
152 |
-
auth['token_refresh_body'] = json.loads(auth['token_refresh_body'])
|
153 |
-
except json.JSONDecodeError:
|
154 |
-
auth['token_refresh_body'] = {}
|
155 |
-
|
156 |
-
@classmethod
|
157 |
-
def save(cls, config: ServiceConfig, username: str) -> None:
|
158 |
-
"""Thread-safe configuration save with optimistic locking"""
|
159 |
-
with cls._file_lock:
|
160 |
-
try:
|
161 |
-
# Convert to dict for JSON serialization
|
162 |
-
config_dict = config.model_dump()
|
163 |
-
|
164 |
-
# Load current config for race condition check
|
165 |
-
try:
|
166 |
-
current_config = cls._load()
|
167 |
-
|
168 |
-
# Check for race condition
|
169 |
-
if config.last_update_date and current_config.last_update_date:
|
170 |
-
if not timestamps_equal(config.last_update_date, current_config.last_update_date):
|
171 |
-
raise RaceConditionError(
|
172 |
-
"Configuration was modified by another user",
|
173 |
-
current_user=username,
|
174 |
-
last_update_user=current_config.last_update_user,
|
175 |
-
last_update_date=current_config.last_update_date,
|
176 |
-
entity_type="configuration"
|
177 |
-
)
|
178 |
-
except ConfigurationError as e:
|
179 |
-
# Eğer mevcut config yüklenemiyorsa, race condition kontrolünü atla
|
180 |
-
log_warning(f"Could not load current config for race condition check: {e}")
|
181 |
-
current_config = None
|
182 |
-
|
183 |
-
# Update metadata
|
184 |
-
config.last_update_date = get_current_timestamp()
|
185 |
-
config.last_update_user = username
|
186 |
-
|
187 |
-
# Convert to JSON - Pydantic v2 kullanımı
|
188 |
-
data = config.model_dump(mode='json')
|
189 |
-
json_str = json.dumps(data, ensure_ascii=False, indent=2)
|
190 |
-
|
191 |
-
# Backup current file if exists
|
192 |
-
backup_path = None
|
193 |
-
if cls._CONFIG_PATH.exists():
|
194 |
-
backup_path = cls._CONFIG_PATH.with_suffix('.backup')
|
195 |
-
shutil.copy2(str(cls._CONFIG_PATH), str(backup_path))
|
196 |
-
log_debug(f"Created backup at {backup_path}")
|
197 |
-
|
198 |
-
try:
|
199 |
-
# Write to temporary file first
|
200 |
-
temp_path = cls._CONFIG_PATH.with_suffix('.tmp')
|
201 |
-
with open(temp_path, 'w', encoding='utf-8') as f:
|
202 |
-
f.write(json_str)
|
203 |
-
|
204 |
-
# Validate the temp file by trying to load it
|
205 |
-
with open(temp_path, 'r', encoding='utf-8') as f:
|
206 |
-
test_data = commentjson.load(f)
|
207 |
-
ServiceConfig.model_validate(test_data)
|
208 |
-
|
209 |
-
# If validation passes, replace the original
|
210 |
-
shutil.move(str(temp_path), str(cls._CONFIG_PATH))
|
211 |
-
|
212 |
-
# Delete backup if save successful
|
213 |
-
if backup_path and backup_path.exists():
|
214 |
-
backup_path.unlink()
|
215 |
-
|
216 |
-
except Exception as e:
|
217 |
-
# Restore from backup if something went wrong
|
218 |
-
if backup_path and backup_path.exists():
|
219 |
-
shutil.move(str(backup_path), str(cls._CONFIG_PATH))
|
220 |
-
log_error(f"Restored configuration from backup due to error: {e}")
|
221 |
-
raise
|
222 |
-
|
223 |
-
# Update cached instance
|
224 |
-
with cls._lock:
|
225 |
-
cls._instance = config
|
226 |
-
|
227 |
-
log_info(
|
228 |
-
"Configuration saved successfully",
|
229 |
-
user=username,
|
230 |
-
last_update=config.last_update_date
|
231 |
-
)
|
232 |
-
|
233 |
-
except Exception as e:
|
234 |
-
log_error(f"Failed to save config", error=str(e))
|
235 |
-
raise ConfigurationError(
|
236 |
-
f"Failed to save configuration: {str(e)}",
|
237 |
-
config_key="service_config.jsonc"
|
238 |
-
)
|
239 |
-
|
240 |
-
# ===================== Environment Methods =====================
|
241 |
-
|
242 |
-
@classmethod
|
243 |
-
def update_environment(cls, update_data: dict, username: str) -> None:
|
244 |
-
"""Update environment configuration"""
|
245 |
-
with cls._lock:
|
246 |
-
config = cls.get()
|
247 |
-
|
248 |
-
# Update providers
|
249 |
-
if 'llm_provider' in update_data:
|
250 |
-
config.global_config.llm_provider = update_data['llm_provider']
|
251 |
-
|
252 |
-
if 'tts_provider' in update_data:
|
253 |
-
config.global_config.tts_provider = update_data['tts_provider']
|
254 |
-
|
255 |
-
if 'stt_provider' in update_data:
|
256 |
-
config.global_config.stt_provider = update_data['stt_provider']
|
257 |
-
|
258 |
-
# Log activity
|
259 |
-
cls._add_activity(
|
260 |
-
config, username, "UPDATE_ENVIRONMENT",
|
261 |
-
"environment", None,
|
262 |
-
f"Updated providers"
|
263 |
-
)
|
264 |
-
|
265 |
-
# Save
|
266 |
-
cls.save(config, username)
|
267 |
-
|
268 |
-
@classmethod
|
269 |
-
def _ensure_providers(cls, config_data: Dict[str, Any]) -> None:
|
270 |
-
"""Ensure config has required provider structure"""
|
271 |
-
if 'config' not in config_data:
|
272 |
-
config_data['config'] = {}
|
273 |
-
|
274 |
-
config = config_data['config']
|
275 |
-
|
276 |
-
# Ensure provider settings exist
|
277 |
-
if 'llm_provider' not in config:
|
278 |
-
config['llm_provider'] = {
|
279 |
-
'name': 'spark_cloud',
|
280 |
-
'api_key': '',
|
281 |
-
'endpoint': 'http://localhost:8080',
|
282 |
-
'settings': {}
|
283 |
-
}
|
284 |
-
|
285 |
-
if 'tts_provider' not in config:
|
286 |
-
config['tts_provider'] = {
|
287 |
-
'name': 'no_tts',
|
288 |
-
'api_key': '',
|
289 |
-
'endpoint': None,
|
290 |
-
'settings': {}
|
291 |
-
}
|
292 |
-
|
293 |
-
if 'stt_provider' not in config:
|
294 |
-
config['stt_provider'] = {
|
295 |
-
'name': 'no_stt',
|
296 |
-
'api_key': '',
|
297 |
-
'endpoint': None,
|
298 |
-
'settings': {}
|
299 |
-
}
|
300 |
-
|
301 |
-
# Ensure providers list exists
|
302 |
-
if 'providers' not in config:
|
303 |
-
config['providers'] = [
|
304 |
-
{
|
305 |
-
"type": "llm",
|
306 |
-
"name": "spark_cloud",
|
307 |
-
"display_name": "Spark LLM (Cloud)",
|
308 |
-
"requires_endpoint": True,
|
309 |
-
"requires_api_key": True,
|
310 |
-
"requires_repo_info": False,
|
311 |
-
"description": "Spark Cloud LLM Service"
|
312 |
-
},
|
313 |
-
{
|
314 |
-
"type": "tts",
|
315 |
-
"name": "no_tts",
|
316 |
-
"display_name": "No TTS",
|
317 |
-
"requires_endpoint": False,
|
318 |
-
"requires_api_key": False,
|
319 |
-
"requires_repo_info": False,
|
320 |
-
"description": "Text-to-Speech disabled"
|
321 |
-
},
|
322 |
-
{
|
323 |
-
"type": "stt",
|
324 |
-
"name": "no_stt",
|
325 |
-
"display_name": "No STT",
|
326 |
-
"requires_endpoint": False,
|
327 |
-
"requires_api_key": False,
|
328 |
-
"requires_repo_info": False,
|
329 |
-
"description": "Speech-to-Text disabled"
|
330 |
-
}
|
331 |
-
]
|
332 |
-
|
333 |
-
# ===================== Project Methods =====================
|
334 |
-
|
335 |
-
@classmethod
|
336 |
-
def get_project(cls, project_id: int) -> Optional[ProjectConfig]:
|
337 |
-
"""Get project by ID"""
|
338 |
-
config = cls.get()
|
339 |
-
return next((p for p in config.projects if p.id == project_id), None)
|
340 |
-
|
341 |
-
@classmethod
|
342 |
-
def create_project(cls, project_data: dict, username: str) -> ProjectConfig:
|
343 |
-
"""Create new project with initial version"""
|
344 |
-
with cls._lock:
|
345 |
-
config = cls.get()
|
346 |
-
|
347 |
-
# Check for duplicate name
|
348 |
-
existing_project = next((p for p in config.projects if p.name == project_data['name'] and not p.deleted), None)
|
349 |
-
if existing_project:
|
350 |
-
raise DuplicateResourceError("Project", project_data['name'])
|
351 |
-
|
352 |
-
|
353 |
-
# Create project
|
354 |
-
project = ProjectConfig(
|
355 |
-
id=config.project_id_counter,
|
356 |
-
created_date=get_current_timestamp(),
|
357 |
-
created_by=username,
|
358 |
-
version_id_counter=1, # Başlangıç değeri
|
359 |
-
versions=[], # Boş başla
|
360 |
-
**project_data
|
361 |
-
)
|
362 |
-
|
363 |
-
# Create initial version with proper models
|
364 |
-
initial_version = VersionConfig(
|
365 |
-
no=1,
|
366 |
-
caption="Initial version",
|
367 |
-
description="Auto-generated initial version",
|
368 |
-
published=False, # Explicitly set to False
|
369 |
-
deleted=False,
|
370 |
-
general_prompt="You are a helpful assistant.",
|
371 |
-
welcome_prompt=None,
|
372 |
-
llm=LLMConfiguration(
|
373 |
-
repo_id="ytu-ce-cosmos/Turkish-Llama-8b-Instruct-v0.1",
|
374 |
-
generation_config=GenerationConfig(
|
375 |
-
max_new_tokens=512,
|
376 |
-
temperature=0.7,
|
377 |
-
top_p=0.9,
|
378 |
-
repetition_penalty=1.1,
|
379 |
-
do_sample=True
|
380 |
-
),
|
381 |
-
use_fine_tune=False,
|
382 |
-
fine_tune_zip=""
|
383 |
-
),
|
384 |
-
intents=[],
|
385 |
-
created_date=get_current_timestamp(),
|
386 |
-
created_by=username,
|
387 |
-
last_update_date=None,
|
388 |
-
last_update_user=None,
|
389 |
-
publish_date=None,
|
390 |
-
published_by=None
|
391 |
-
)
|
392 |
-
|
393 |
-
# Add initial version to project
|
394 |
-
project.versions.append(initial_version)
|
395 |
-
project.version_id_counter = 2 # Next version will be 2
|
396 |
-
|
397 |
-
# Update config
|
398 |
-
config.projects.append(project)
|
399 |
-
config.project_id_counter += 1
|
400 |
-
|
401 |
-
# Log activity
|
402 |
-
cls._add_activity(
|
403 |
-
config, username, "CREATE_PROJECT",
|
404 |
-
"project", project.name,
|
405 |
-
f"Created with initial version"
|
406 |
-
)
|
407 |
-
|
408 |
-
# Save
|
409 |
-
cls.save(config, username)
|
410 |
-
|
411 |
-
log_info(
|
412 |
-
"Project created with initial version",
|
413 |
-
project_id=project.id,
|
414 |
-
name=project.name,
|
415 |
-
user=username
|
416 |
-
)
|
417 |
-
|
418 |
-
return project
|
419 |
-
|
420 |
-
@classmethod
|
421 |
-
def update_project(cls, project_id: int, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> ProjectConfig:
|
422 |
-
"""Update project with optimistic locking"""
|
423 |
-
with cls._lock:
|
424 |
-
config = cls.get()
|
425 |
-
project = cls.get_project(project_id)
|
426 |
-
|
427 |
-
if not project:
|
428 |
-
raise ResourceNotFoundError("project", project_id)
|
429 |
-
|
430 |
-
# Check race condition
|
431 |
-
if expected_last_update is not None and expected_last_update != '':
|
432 |
-
if project.last_update_date and not timestamps_equal(expected_last_update, project.last_update_date):
|
433 |
-
raise RaceConditionError(
|
434 |
-
f"Project '{project.name}' was modified by another user",
|
435 |
-
current_user=username,
|
436 |
-
last_update_user=project.last_update_user,
|
437 |
-
last_update_date=project.last_update_date,
|
438 |
-
entity_type="project",
|
439 |
-
entity_id=project_id
|
440 |
-
)
|
441 |
-
|
442 |
-
# Update fields
|
443 |
-
for key, value in update_data.items():
|
444 |
-
if hasattr(project, key) and key not in ['id', 'created_date', 'created_by', 'last_update_date', 'last_update_user']:
|
445 |
-
setattr(project, key, value)
|
446 |
-
|
447 |
-
project.last_update_date = get_current_timestamp()
|
448 |
-
project.last_update_user = username
|
449 |
-
|
450 |
-
cls._add_activity(
|
451 |
-
config, username, "UPDATE_PROJECT",
|
452 |
-
"project", project.name
|
453 |
-
)
|
454 |
-
|
455 |
-
# Save
|
456 |
-
cls.save(config, username)
|
457 |
-
|
458 |
-
log_info(
|
459 |
-
"Project updated",
|
460 |
-
project_id=project.id,
|
461 |
-
user=username
|
462 |
-
)
|
463 |
-
|
464 |
-
return project
|
465 |
-
|
466 |
-
@classmethod
|
467 |
-
def delete_project(cls, project_id: int, username: str) -> None:
|
468 |
-
"""Soft delete project"""
|
469 |
-
with cls._lock:
|
470 |
-
config = cls.get()
|
471 |
-
project = cls.get_project(project_id)
|
472 |
-
|
473 |
-
if not project:
|
474 |
-
raise ResourceNotFoundError("project", project_id)
|
475 |
-
|
476 |
-
project.deleted = True
|
477 |
-
project.last_update_date = get_current_timestamp()
|
478 |
-
project.last_update_user = username
|
479 |
-
|
480 |
-
cls._add_activity(
|
481 |
-
config, username, "DELETE_PROJECT",
|
482 |
-
"project", project.name
|
483 |
-
)
|
484 |
-
|
485 |
-
# Save
|
486 |
-
cls.save(config, username)
|
487 |
-
|
488 |
-
log_info(
|
489 |
-
"Project deleted",
|
490 |
-
project_id=project.id,
|
491 |
-
user=username
|
492 |
-
)
|
493 |
-
|
494 |
-
@classmethod
|
495 |
-
def toggle_project(cls, project_id: int, username: str) -> bool:
|
496 |
-
"""Toggle project enabled status"""
|
497 |
-
with cls._lock:
|
498 |
-
config = cls.get()
|
499 |
-
project = cls.get_project(project_id)
|
500 |
-
|
501 |
-
if not project:
|
502 |
-
raise ResourceNotFoundError("project", project_id)
|
503 |
-
|
504 |
-
project.enabled = not project.enabled
|
505 |
-
project.last_update_date = get_current_timestamp()
|
506 |
-
project.last_update_user = username
|
507 |
-
|
508 |
-
# Log activity
|
509 |
-
cls._add_activity(
|
510 |
-
config, username, "TOGGLE_PROJECT",
|
511 |
-
"project", project.name,
|
512 |
-
f"{'Enabled' if project.enabled else 'Disabled'}"
|
513 |
-
)
|
514 |
-
|
515 |
-
# Save
|
516 |
-
cls.save(config, username)
|
517 |
-
|
518 |
-
log_info(
|
519 |
-
"Project toggled",
|
520 |
-
project_id=project.id,
|
521 |
-
enabled=project.enabled,
|
522 |
-
user=username
|
523 |
-
)
|
524 |
-
|
525 |
-
return project.enabled
|
526 |
-
|
527 |
-
# ===================== Version Methods =====================
|
528 |
-
|
529 |
-
@classmethod
|
530 |
-
def create_version(cls, project_id: int, version_data: dict, username: str) -> VersionConfig:
|
531 |
-
"""Create new version"""
|
532 |
-
with cls._lock:
|
533 |
-
config = cls.get()
|
534 |
-
project = cls.get_project(project_id)
|
535 |
-
|
536 |
-
if not project:
|
537 |
-
raise ResourceNotFoundError("project", project_id)
|
538 |
-
|
539 |
-
# Handle source version copy
|
540 |
-
if 'source_version_no' in version_data and version_data['source_version_no']:
|
541 |
-
source_version = next((v for v in project.versions if v.no == version_data['source_version_no']), None)
|
542 |
-
if source_version:
|
543 |
-
# Copy from source version
|
544 |
-
version_dict = source_version.model_dump()
|
545 |
-
# Remove fields that shouldn't be copied
|
546 |
-
for field in ['no', 'created_date', 'created_by', 'published', 'publish_date',
|
547 |
-
'published_by', 'last_update_date', 'last_update_user']:
|
548 |
-
version_dict.pop(field, None)
|
549 |
-
# Override with provided data
|
550 |
-
version_dict['caption'] = version_data.get('caption', f"Copy of {source_version.caption}")
|
551 |
-
else:
|
552 |
-
# Source not found, create blank
|
553 |
-
version_dict = {
|
554 |
-
'caption': version_data.get('caption', 'New Version'),
|
555 |
-
'general_prompt': '',
|
556 |
-
'welcome_prompt': None,
|
557 |
-
'llm': {
|
558 |
-
'repo_id': '',
|
559 |
-
'generation_config': {
|
560 |
-
'max_new_tokens': 512,
|
561 |
-
'temperature': 0.7,
|
562 |
-
'top_p': 0.95,
|
563 |
-
'repetition_penalty': 1.1
|
564 |
-
},
|
565 |
-
'use_fine_tune': False,
|
566 |
-
'fine_tune_zip': ''
|
567 |
-
},
|
568 |
-
'intents': []
|
569 |
-
}
|
570 |
-
else:
|
571 |
-
# Create blank version
|
572 |
-
version_dict = {
|
573 |
-
'caption': version_data.get('caption', 'New Version'),
|
574 |
-
'general_prompt': '',
|
575 |
-
'welcome_prompt': None,
|
576 |
-
'llm': {
|
577 |
-
'repo_id': '',
|
578 |
-
'generation_config': {
|
579 |
-
'max_new_tokens': 512,
|
580 |
-
'temperature': 0.7,
|
581 |
-
'top_p': 0.95,
|
582 |
-
'repetition_penalty': 1.1
|
583 |
-
},
|
584 |
-
'use_fine_tune': False,
|
585 |
-
'fine_tune_zip': ''
|
586 |
-
},
|
587 |
-
'intents': []
|
588 |
-
}
|
589 |
-
|
590 |
-
# Create version
|
591 |
-
version = VersionConfig(
|
592 |
-
no=project.version_id_counter,
|
593 |
-
published=False, # New versions are always unpublished
|
594 |
-
deleted=False,
|
595 |
-
created_date=get_current_timestamp(),
|
596 |
-
created_by=username,
|
597 |
-
last_update_date=None,
|
598 |
-
last_update_user=None,
|
599 |
-
publish_date=None,
|
600 |
-
published_by=None,
|
601 |
-
**version_dict
|
602 |
-
)
|
603 |
-
|
604 |
-
# Update project
|
605 |
-
project.versions.append(version)
|
606 |
-
project.version_id_counter += 1
|
607 |
-
project.last_update_date = get_current_timestamp()
|
608 |
-
project.last_update_user = username
|
609 |
-
|
610 |
-
# Log activity
|
611 |
-
cls._add_activity(
|
612 |
-
config, username, "CREATE_VERSION",
|
613 |
-
"version", version.no, f"{project.name} v{version.no}",
|
614 |
-
f"Project: {project.name}"
|
615 |
-
)
|
616 |
-
|
617 |
-
# Save
|
618 |
-
cls.save(config, username)
|
619 |
-
|
620 |
-
log_info(
|
621 |
-
"Version created",
|
622 |
-
project_id=project.id,
|
623 |
-
version_no=version.no,
|
624 |
-
user=username
|
625 |
-
)
|
626 |
-
|
627 |
-
return version
|
628 |
-
|
629 |
-
@classmethod
|
630 |
-
def publish_version(cls, project_id: int, version_no: int, username: str) -> tuple[ProjectConfig, VersionConfig]:
|
631 |
-
"""Publish a version"""
|
632 |
-
with cls._lock:
|
633 |
-
config = cls.get()
|
634 |
-
project = cls.get_project(project_id)
|
635 |
-
|
636 |
-
if not project:
|
637 |
-
raise ResourceNotFoundError("project", project_id)
|
638 |
-
|
639 |
-
version = next((v for v in project.versions if v.no == version_no), None)
|
640 |
-
if not version:
|
641 |
-
raise ResourceNotFoundError("version", version_no)
|
642 |
-
|
643 |
-
# Unpublish other versions
|
644 |
-
for v in project.versions:
|
645 |
-
if v.published and v.no != version_no:
|
646 |
-
v.published = False
|
647 |
-
|
648 |
-
# Publish this version
|
649 |
-
version.published = True
|
650 |
-
version.publish_date = get_current_timestamp()
|
651 |
-
version.published_by = username
|
652 |
-
|
653 |
-
# Update project
|
654 |
-
project.last_update_date = get_current_timestamp()
|
655 |
-
project.last_update_user = username
|
656 |
-
|
657 |
-
# Log activity
|
658 |
-
cls._add_activity(
|
659 |
-
config, username, "PUBLISH_VERSION",
|
660 |
-
"version", f"{project.name} v{version.no}"
|
661 |
-
)
|
662 |
-
|
663 |
-
# Save
|
664 |
-
cls.save(config, username)
|
665 |
-
|
666 |
-
log_info(
|
667 |
-
"Version published",
|
668 |
-
project_id=project.id,
|
669 |
-
version_no=version.no,
|
670 |
-
user=username
|
671 |
-
)
|
672 |
-
|
673 |
-
return project, version
|
674 |
-
|
675 |
-
@classmethod
|
676 |
-
def update_version(cls, project_id: int, version_no: int, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> VersionConfig:
|
677 |
-
"""Update version with optimistic locking"""
|
678 |
-
with cls._lock:
|
679 |
-
config = cls.get()
|
680 |
-
project = cls.get_project(project_id)
|
681 |
-
|
682 |
-
if not project:
|
683 |
-
raise ResourceNotFoundError("project", project_id)
|
684 |
-
|
685 |
-
version = next((v for v in project.versions if v.no == version_no), None)
|
686 |
-
if not version:
|
687 |
-
raise ResourceNotFoundError("version", version_no)
|
688 |
-
|
689 |
-
# Ensure published is a boolean (safety check)
|
690 |
-
if version.published is None:
|
691 |
-
version.published = False
|
692 |
-
|
693 |
-
# Published versions cannot be edited
|
694 |
-
if version.published:
|
695 |
-
raise ValidationError("Published versions cannot be modified")
|
696 |
-
|
697 |
-
# Check race condition
|
698 |
-
if expected_last_update is not None and expected_last_update != '':
|
699 |
-
if version.last_update_date and not timestamps_equal(expected_last_update, version.last_update_date):
|
700 |
-
raise RaceConditionError(
|
701 |
-
f"Version '{version.no}' was modified by another user",
|
702 |
-
current_user=username,
|
703 |
-
last_update_user=version.last_update_user,
|
704 |
-
last_update_date=version.last_update_date,
|
705 |
-
entity_type="version",
|
706 |
-
entity_id=f"{project_id}:{version_no}"
|
707 |
-
)
|
708 |
-
|
709 |
-
# Update fields
|
710 |
-
for key, value in update_data.items():
|
711 |
-
if hasattr(version, key) and key not in ['no', 'created_date', 'created_by', 'published', 'last_update_date']:
|
712 |
-
# Handle LLM config
|
713 |
-
if key == 'llm' and isinstance(value, dict):
|
714 |
-
setattr(version, key, LLMConfiguration(**value))
|
715 |
-
# Handle intents
|
716 |
-
elif key == 'intents' and isinstance(value, list):
|
717 |
-
intents = []
|
718 |
-
for intent_data in value:
|
719 |
-
if isinstance(intent_data, dict):
|
720 |
-
intents.append(IntentConfig(**intent_data))
|
721 |
-
else:
|
722 |
-
intents.append(intent_data)
|
723 |
-
setattr(version, key, intents)
|
724 |
-
else:
|
725 |
-
setattr(version, key, value)
|
726 |
-
|
727 |
-
version.last_update_date = get_current_timestamp()
|
728 |
-
version.last_update_user = username
|
729 |
-
|
730 |
-
# Update project last update
|
731 |
-
project.last_update_date = get_current_timestamp()
|
732 |
-
project.last_update_user = username
|
733 |
-
|
734 |
-
# Log activity
|
735 |
-
cls._add_activity(
|
736 |
-
config, username, "UPDATE_VERSION",
|
737 |
-
"version", f"{project.name} v{version.no}"
|
738 |
-
)
|
739 |
-
|
740 |
-
# Save
|
741 |
-
cls.save(config, username)
|
742 |
-
|
743 |
-
log_info(
|
744 |
-
"Version updated",
|
745 |
-
project_id=project.id,
|
746 |
-
version_no=version.no,
|
747 |
-
user=username
|
748 |
-
)
|
749 |
-
|
750 |
-
return version
|
751 |
-
|
752 |
-
@classmethod
|
753 |
-
def delete_version(cls, project_id: int, version_no: int, username: str) -> None:
|
754 |
-
"""Soft delete version"""
|
755 |
-
with cls._lock:
|
756 |
-
config = cls.get()
|
757 |
-
project = cls.get_project(project_id)
|
758 |
-
|
759 |
-
if not project:
|
760 |
-
raise ResourceNotFoundError("project", project_id)
|
761 |
-
|
762 |
-
version = next((v for v in project.versions if v.no == version_no), None)
|
763 |
-
if not version:
|
764 |
-
raise ResourceNotFoundError("version", version_no)
|
765 |
-
|
766 |
-
if version.published:
|
767 |
-
raise ValidationError("Cannot delete published version")
|
768 |
-
|
769 |
-
version.deleted = True
|
770 |
-
version.last_update_date = get_current_timestamp()
|
771 |
-
version.last_update_user = username
|
772 |
-
|
773 |
-
# Update project
|
774 |
-
project.last_update_date = get_current_timestamp()
|
775 |
-
project.last_update_user = username
|
776 |
-
|
777 |
-
# Log activity
|
778 |
-
cls._add_activity(
|
779 |
-
config, username, "DELETE_VERSION",
|
780 |
-
"version", f"{project.name} v{version.no}"
|
781 |
-
)
|
782 |
-
|
783 |
-
# Save
|
784 |
-
cls.save(config, username)
|
785 |
-
|
786 |
-
log_info(
|
787 |
-
"Version deleted",
|
788 |
-
project_id=project.id,
|
789 |
-
version_no=version.no,
|
790 |
-
user=username
|
791 |
-
)
|
792 |
-
|
793 |
-
# ===================== API Methods =====================
|
794 |
-
@classmethod
|
795 |
-
def create_api(cls, api_data: dict, username: str) -> APIConfig:
|
796 |
-
"""Create new API"""
|
797 |
-
with cls._lock:
|
798 |
-
config = cls.get()
|
799 |
-
|
800 |
-
# Check for duplicate name
|
801 |
-
existing_api = next((a for a in config.apis if a.name == api_data['name'] and not a.deleted), None)
|
802 |
-
if existing_api:
|
803 |
-
raise DuplicateResourceError("API", api_data['name'])
|
804 |
-
|
805 |
-
# Create API
|
806 |
-
api = APIConfig(
|
807 |
-
created_date=get_current_timestamp(),
|
808 |
-
created_by=username,
|
809 |
-
**api_data
|
810 |
-
)
|
811 |
-
|
812 |
-
# Add to config
|
813 |
-
config.apis.append(api)
|
814 |
-
|
815 |
-
# Rebuild index
|
816 |
-
config.build_index()
|
817 |
-
|
818 |
-
# Log activity
|
819 |
-
cls._add_activity(
|
820 |
-
config, username, "CREATE_API",
|
821 |
-
"api", api.name
|
822 |
-
)
|
823 |
-
|
824 |
-
# Save
|
825 |
-
cls.save(config, username)
|
826 |
-
|
827 |
-
log_info(
|
828 |
-
"API created",
|
829 |
-
api_name=api.name,
|
830 |
-
user=username
|
831 |
-
)
|
832 |
-
|
833 |
-
return api
|
834 |
-
|
835 |
-
@classmethod
|
836 |
-
def update_api(cls, api_name: str, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> APIConfig:
|
837 |
-
"""Update API with optimistic locking"""
|
838 |
-
with cls._lock:
|
839 |
-
config = cls.get()
|
840 |
-
api = config.get_api(api_name)
|
841 |
-
|
842 |
-
if not api:
|
843 |
-
raise ResourceNotFoundError("api", api_name)
|
844 |
-
|
845 |
-
# Check race condition
|
846 |
-
if expected_last_update is not None and expected_last_update != '':
|
847 |
-
if api.last_update_date and not timestamps_equal(expected_last_update, api.last_update_date):
|
848 |
-
raise RaceConditionError(
|
849 |
-
f"API '{api.name}' was modified by another user",
|
850 |
-
current_user=username,
|
851 |
-
last_update_user=api.last_update_user,
|
852 |
-
last_update_date=api.last_update_date,
|
853 |
-
entity_type="api",
|
854 |
-
entity_id=api.name
|
855 |
-
)
|
856 |
-
|
857 |
-
# Update fields
|
858 |
-
for key, value in update_data.items():
|
859 |
-
if hasattr(api, key) and key not in ['name', 'created_date', 'created_by', 'last_update_date']:
|
860 |
-
setattr(api, key, value)
|
861 |
-
|
862 |
-
api.last_update_date = get_current_timestamp()
|
863 |
-
api.last_update_user = username
|
864 |
-
|
865 |
-
# Rebuild index
|
866 |
-
config.build_index()
|
867 |
-
|
868 |
-
# Log activity
|
869 |
-
cls._add_activity(
|
870 |
-
config, username, "UPDATE_API",
|
871 |
-
"api", api.name
|
872 |
-
)
|
873 |
-
|
874 |
-
# Save
|
875 |
-
cls.save(config, username)
|
876 |
-
|
877 |
-
log_info(
|
878 |
-
"API updated",
|
879 |
-
api_name=api.name,
|
880 |
-
user=username
|
881 |
-
)
|
882 |
-
|
883 |
-
return api
|
884 |
-
|
885 |
-
@classmethod
|
886 |
-
def delete_api(cls, api_name: str, username: str) -> None:
|
887 |
-
"""Soft delete API"""
|
888 |
-
with cls._lock:
|
889 |
-
config = cls.get()
|
890 |
-
api = config.get_api(api_name)
|
891 |
-
|
892 |
-
if not api:
|
893 |
-
raise ResourceNotFoundError("api", api_name)
|
894 |
-
|
895 |
-
api.deleted = True
|
896 |
-
api.last_update_date = get_current_timestamp()
|
897 |
-
api.last_update_user = username
|
898 |
-
|
899 |
-
# Rebuild index
|
900 |
-
config.build_index()
|
901 |
-
|
902 |
-
# Log activity
|
903 |
-
cls._add_activity(
|
904 |
-
config, username, "DELETE_API",
|
905 |
-
"api", api.name
|
906 |
-
)
|
907 |
-
|
908 |
-
# Save
|
909 |
-
cls.save(config, username)
|
910 |
-
|
911 |
-
log_info(
|
912 |
-
"API deleted",
|
913 |
-
api_name=api.name,
|
914 |
-
user=username
|
915 |
-
)
|
916 |
-
|
917 |
-
# ===================== Activity Methods =====================
|
918 |
-
@classmethod
|
919 |
-
def _add_activity(
|
920 |
-
cls,
|
921 |
-
config: ServiceConfig,
|
922 |
-
username: str,
|
923 |
-
action: str,
|
924 |
-
entity_type: str,
|
925 |
-
entity_name: Optional[str] = None,
|
926 |
-
details: Optional[str] = None
|
927 |
-
) -> None:
|
928 |
-
"""Add activity log entry"""
|
929 |
-
# Activity ID'sini oluştur - mevcut en yüksek ID'yi bul
|
930 |
-
max_id = 0
|
931 |
-
if config.activity_log:
|
932 |
-
max_id = max((entry.id for entry in config.activity_log if entry.id), default=0)
|
933 |
-
|
934 |
-
activity_id = max_id + 1
|
935 |
-
|
936 |
-
activity = ActivityLogEntry(
|
937 |
-
id=activity_id,
|
938 |
-
timestamp=get_current_timestamp(),
|
939 |
-
username=username,
|
940 |
-
action=action,
|
941 |
-
entity_type=entity_type,
|
942 |
-
entity_name=entity_name,
|
943 |
-
details=details
|
944 |
-
)
|
945 |
-
|
946 |
-
config.activity_log.append(activity)
|
947 |
-
|
948 |
-
# Keep only last 1000 entries
|
949 |
-
if len(config.activity_log) > 1000:
|
950 |
config.activity_log = config.activity_log[-1000:]
|
|
|
1 |
+
"""
|
2 |
+
Thread-Safe Configuration Provider for Flare Platform
|
3 |
+
"""
|
4 |
+
import threading
|
5 |
+
import os
|
6 |
+
import json
|
7 |
+
import commentjson
|
8 |
+
from typing import Optional, Dict, List, Any
|
9 |
+
from datetime import datetime
|
10 |
+
from pathlib import Path
|
11 |
+
import tempfile
|
12 |
+
import shutil
|
13 |
+
from utils import get_current_timestamp, normalize_timestamp, timestamps_equal
|
14 |
+
|
15 |
+
from .config_models import (
|
16 |
+
ServiceConfig, GlobalConfig, ProjectConfig, VersionConfig,
|
17 |
+
IntentConfig, APIConfig, ActivityLogEntry, ParameterConfig,
|
18 |
+
LLMConfiguration, GenerationConfig
|
19 |
+
)
|
20 |
+
from utils.logger import log_info, log_error, log_warning, log_debug, LogTimer
|
21 |
+
from utils.exceptions import (
|
22 |
+
RaceConditionError, ConfigurationError, ResourceNotFoundError,
|
23 |
+
DuplicateResourceError, ValidationError
|
24 |
+
)
|
25 |
+
from utils.encryption_utils import encrypt, decrypt
|
26 |
+
|
27 |
+
class ConfigProvider:
|
28 |
+
"""Thread-safe singleton configuration provider"""
|
29 |
+
|
30 |
+
_instance: Optional[ServiceConfig] = None
|
31 |
+
_lock = threading.RLock() # Reentrant lock for nested calls
|
32 |
+
_file_lock = threading.Lock() # Separate lock for file operations
|
33 |
+
_CONFIG_PATH = Path(__file__).parent.parent / "service_config.jsonc"
|
34 |
+
|
35 |
+
@staticmethod
|
36 |
+
def _normalize_date(date_str: Optional[str]) -> str:
|
37 |
+
"""Normalize date string for comparison"""
|
38 |
+
if not date_str:
|
39 |
+
return ""
|
40 |
+
return date_str.replace(' ', 'T').replace('+00:00', 'Z').replace('.000Z', 'Z')
|
41 |
+
|
42 |
+
@classmethod
|
43 |
+
def get(cls) -> ServiceConfig:
|
44 |
+
"""Get cached configuration - thread-safe"""
|
45 |
+
if cls._instance is None:
|
46 |
+
with cls._lock:
|
47 |
+
# Double-checked locking pattern
|
48 |
+
if cls._instance is None:
|
49 |
+
with LogTimer("config_load"):
|
50 |
+
cls._instance = cls._load()
|
51 |
+
cls._instance.build_index()
|
52 |
+
log_info("Configuration loaded successfully")
|
53 |
+
return cls._instance
|
54 |
+
|
55 |
+
@classmethod
|
56 |
+
def reload(cls) -> ServiceConfig:
|
57 |
+
"""Force reload configuration from file"""
|
58 |
+
with cls._lock:
|
59 |
+
log_info("Reloading configuration...")
|
60 |
+
cls._instance = None
|
61 |
+
return cls.get()
|
62 |
+
|
63 |
+
@classmethod
|
64 |
+
def _load(cls) -> ServiceConfig:
|
65 |
+
"""Load configuration from file"""
|
66 |
+
try:
|
67 |
+
if not cls._CONFIG_PATH.exists():
|
68 |
+
raise ConfigurationError(
|
69 |
+
f"Config file not found: {cls._CONFIG_PATH}",
|
70 |
+
config_key="service_config.jsonc"
|
71 |
+
)
|
72 |
+
|
73 |
+
with open(cls._CONFIG_PATH, 'r', encoding='utf-8') as f:
|
74 |
+
config_data = commentjson.load(f)
|
75 |
+
|
76 |
+
# Debug: İlk project'in tarihini kontrol et
|
77 |
+
if 'projects' in config_data and len(config_data['projects']) > 0:
|
78 |
+
first_project = config_data['projects'][0]
|
79 |
+
log_debug(f"🔍 Raw project data - last_update_date: {first_project.get('last_update_date')}")
|
80 |
+
|
81 |
+
# Ensure required fields
|
82 |
+
if 'config' not in config_data:
|
83 |
+
config_data['config'] = {}
|
84 |
+
|
85 |
+
# Ensure providers exist
|
86 |
+
cls._ensure_providers(config_data)
|
87 |
+
|
88 |
+
# Parse API configs (handle JSON strings)
|
89 |
+
if 'apis' in config_data:
|
90 |
+
cls._parse_api_configs(config_data['apis'])
|
91 |
+
|
92 |
+
# Validate and create model
|
93 |
+
cfg = ServiceConfig.model_validate(config_data)
|
94 |
+
|
95 |
+
# Debug: Model'e dönüştükten sonra kontrol et
|
96 |
+
if cfg.projects and len(cfg.projects) > 0:
|
97 |
+
log_debug(f"🔍 Parsed project - last_update_date: {cfg.projects[0].last_update_date}")
|
98 |
+
log_debug(f"🔍 Type: {type(cfg.projects[0].last_update_date)}")
|
99 |
+
|
100 |
+
# Log versions published status after parsing
|
101 |
+
for version in cfg.projects[0].versions:
|
102 |
+
log_debug(f"🔍 Parsed version {version.no} - published: {version.published} (type: {type(version.published)})")
|
103 |
+
|
104 |
+
log_debug(
|
105 |
+
"Configuration loaded",
|
106 |
+
projects=len(cfg.projects),
|
107 |
+
apis=len(cfg.apis),
|
108 |
+
users=len(cfg.global_config.users)
|
109 |
+
)
|
110 |
+
|
111 |
+
return cfg
|
112 |
+
|
113 |
+
except Exception as e:
|
114 |
+
log_error(f"Error loading config", error=str(e), path=str(cls._CONFIG_PATH))
|
115 |
+
raise ConfigurationError(f"Failed to load configuration: {e}")
|
116 |
+
|
117 |
+
@classmethod
|
118 |
+
def _parse_api_configs(cls, apis: List[Dict[str, Any]]) -> None:
|
119 |
+
"""Parse JSON string fields in API configs"""
|
120 |
+
for api in apis:
|
121 |
+
# Parse headers
|
122 |
+
if 'headers' in api and isinstance(api['headers'], str):
|
123 |
+
try:
|
124 |
+
api['headers'] = json.loads(api['headers'])
|
125 |
+
except json.JSONDecodeError:
|
126 |
+
api['headers'] = {}
|
127 |
+
|
128 |
+
# Parse body_template
|
129 |
+
if 'body_template' in api and isinstance(api['body_template'], str):
|
130 |
+
try:
|
131 |
+
api['body_template'] = json.loads(api['body_template'])
|
132 |
+
except json.JSONDecodeError:
|
133 |
+
api['body_template'] = {}
|
134 |
+
|
135 |
+
# Parse auth configs
|
136 |
+
if 'auth' in api and api['auth']:
|
137 |
+
cls._parse_auth_config(api['auth'])
|
138 |
+
|
139 |
+
@classmethod
|
140 |
+
def _parse_auth_config(cls, auth: Dict[str, Any]) -> None:
|
141 |
+
"""Parse auth configuration"""
|
142 |
+
# Parse token_request_body
|
143 |
+
if 'token_request_body' in auth and isinstance(auth['token_request_body'], str):
|
144 |
+
try:
|
145 |
+
auth['token_request_body'] = json.loads(auth['token_request_body'])
|
146 |
+
except json.JSONDecodeError:
|
147 |
+
auth['token_request_body'] = {}
|
148 |
+
|
149 |
+
# Parse token_refresh_body
|
150 |
+
if 'token_refresh_body' in auth and isinstance(auth['token_refresh_body'], str):
|
151 |
+
try:
|
152 |
+
auth['token_refresh_body'] = json.loads(auth['token_refresh_body'])
|
153 |
+
except json.JSONDecodeError:
|
154 |
+
auth['token_refresh_body'] = {}
|
155 |
+
|
156 |
+
@classmethod
|
157 |
+
def save(cls, config: ServiceConfig, username: str) -> None:
|
158 |
+
"""Thread-safe configuration save with optimistic locking"""
|
159 |
+
with cls._file_lock:
|
160 |
+
try:
|
161 |
+
# Convert to dict for JSON serialization
|
162 |
+
config_dict = config.model_dump()
|
163 |
+
|
164 |
+
# Load current config for race condition check
|
165 |
+
try:
|
166 |
+
current_config = cls._load()
|
167 |
+
|
168 |
+
# Check for race condition
|
169 |
+
if config.last_update_date and current_config.last_update_date:
|
170 |
+
if not timestamps_equal(config.last_update_date, current_config.last_update_date):
|
171 |
+
raise RaceConditionError(
|
172 |
+
"Configuration was modified by another user",
|
173 |
+
current_user=username,
|
174 |
+
last_update_user=current_config.last_update_user,
|
175 |
+
last_update_date=current_config.last_update_date,
|
176 |
+
entity_type="configuration"
|
177 |
+
)
|
178 |
+
except ConfigurationError as e:
|
179 |
+
# Eğer mevcut config yüklenemiyorsa, race condition kontrolünü atla
|
180 |
+
log_warning(f"Could not load current config for race condition check: {e}")
|
181 |
+
current_config = None
|
182 |
+
|
183 |
+
# Update metadata
|
184 |
+
config.last_update_date = get_current_timestamp()
|
185 |
+
config.last_update_user = username
|
186 |
+
|
187 |
+
# Convert to JSON - Pydantic v2 kullanımı
|
188 |
+
data = config.model_dump(mode='json')
|
189 |
+
json_str = json.dumps(data, ensure_ascii=False, indent=2)
|
190 |
+
|
191 |
+
# Backup current file if exists
|
192 |
+
backup_path = None
|
193 |
+
if cls._CONFIG_PATH.exists():
|
194 |
+
backup_path = cls._CONFIG_PATH.with_suffix('.backup')
|
195 |
+
shutil.copy2(str(cls._CONFIG_PATH), str(backup_path))
|
196 |
+
log_debug(f"Created backup at {backup_path}")
|
197 |
+
|
198 |
+
try:
|
199 |
+
# Write to temporary file first
|
200 |
+
temp_path = cls._CONFIG_PATH.with_suffix('.tmp')
|
201 |
+
with open(temp_path, 'w', encoding='utf-8') as f:
|
202 |
+
f.write(json_str)
|
203 |
+
|
204 |
+
# Validate the temp file by trying to load it
|
205 |
+
with open(temp_path, 'r', encoding='utf-8') as f:
|
206 |
+
test_data = commentjson.load(f)
|
207 |
+
ServiceConfig.model_validate(test_data)
|
208 |
+
|
209 |
+
# If validation passes, replace the original
|
210 |
+
shutil.move(str(temp_path), str(cls._CONFIG_PATH))
|
211 |
+
|
212 |
+
# Delete backup if save successful
|
213 |
+
if backup_path and backup_path.exists():
|
214 |
+
backup_path.unlink()
|
215 |
+
|
216 |
+
except Exception as e:
|
217 |
+
# Restore from backup if something went wrong
|
218 |
+
if backup_path and backup_path.exists():
|
219 |
+
shutil.move(str(backup_path), str(cls._CONFIG_PATH))
|
220 |
+
log_error(f"Restored configuration from backup due to error: {e}")
|
221 |
+
raise
|
222 |
+
|
223 |
+
# Update cached instance
|
224 |
+
with cls._lock:
|
225 |
+
cls._instance = config
|
226 |
+
|
227 |
+
log_info(
|
228 |
+
"Configuration saved successfully",
|
229 |
+
user=username,
|
230 |
+
last_update=config.last_update_date
|
231 |
+
)
|
232 |
+
|
233 |
+
except Exception as e:
|
234 |
+
log_error(f"Failed to save config", error=str(e))
|
235 |
+
raise ConfigurationError(
|
236 |
+
f"Failed to save configuration: {str(e)}",
|
237 |
+
config_key="service_config.jsonc"
|
238 |
+
)
|
239 |
+
|
240 |
+
# ===================== Environment Methods =====================
|
241 |
+
|
242 |
+
@classmethod
|
243 |
+
def update_environment(cls, update_data: dict, username: str) -> None:
|
244 |
+
"""Update environment configuration"""
|
245 |
+
with cls._lock:
|
246 |
+
config = cls.get()
|
247 |
+
|
248 |
+
# Update providers
|
249 |
+
if 'llm_provider' in update_data:
|
250 |
+
config.global_config.llm_provider = update_data['llm_provider']
|
251 |
+
|
252 |
+
if 'tts_provider' in update_data:
|
253 |
+
config.global_config.tts_provider = update_data['tts_provider']
|
254 |
+
|
255 |
+
if 'stt_provider' in update_data:
|
256 |
+
config.global_config.stt_provider = update_data['stt_provider']
|
257 |
+
|
258 |
+
# Log activity
|
259 |
+
cls._add_activity(
|
260 |
+
config, username, "UPDATE_ENVIRONMENT",
|
261 |
+
"environment", None,
|
262 |
+
f"Updated providers"
|
263 |
+
)
|
264 |
+
|
265 |
+
# Save
|
266 |
+
cls.save(config, username)
|
267 |
+
|
268 |
+
@classmethod
|
269 |
+
def _ensure_providers(cls, config_data: Dict[str, Any]) -> None:
|
270 |
+
"""Ensure config has required provider structure"""
|
271 |
+
if 'config' not in config_data:
|
272 |
+
config_data['config'] = {}
|
273 |
+
|
274 |
+
config = config_data['config']
|
275 |
+
|
276 |
+
# Ensure provider settings exist
|
277 |
+
if 'llm_provider' not in config:
|
278 |
+
config['llm_provider'] = {
|
279 |
+
'name': 'spark_cloud',
|
280 |
+
'api_key': '',
|
281 |
+
'endpoint': 'http://localhost:8080',
|
282 |
+
'settings': {}
|
283 |
+
}
|
284 |
+
|
285 |
+
if 'tts_provider' not in config:
|
286 |
+
config['tts_provider'] = {
|
287 |
+
'name': 'no_tts',
|
288 |
+
'api_key': '',
|
289 |
+
'endpoint': None,
|
290 |
+
'settings': {}
|
291 |
+
}
|
292 |
+
|
293 |
+
if 'stt_provider' not in config:
|
294 |
+
config['stt_provider'] = {
|
295 |
+
'name': 'no_stt',
|
296 |
+
'api_key': '',
|
297 |
+
'endpoint': None,
|
298 |
+
'settings': {}
|
299 |
+
}
|
300 |
+
|
301 |
+
# Ensure providers list exists
|
302 |
+
if 'providers' not in config:
|
303 |
+
config['providers'] = [
|
304 |
+
{
|
305 |
+
"type": "llm",
|
306 |
+
"name": "spark_cloud",
|
307 |
+
"display_name": "Spark LLM (Cloud)",
|
308 |
+
"requires_endpoint": True,
|
309 |
+
"requires_api_key": True,
|
310 |
+
"requires_repo_info": False,
|
311 |
+
"description": "Spark Cloud LLM Service"
|
312 |
+
},
|
313 |
+
{
|
314 |
+
"type": "tts",
|
315 |
+
"name": "no_tts",
|
316 |
+
"display_name": "No TTS",
|
317 |
+
"requires_endpoint": False,
|
318 |
+
"requires_api_key": False,
|
319 |
+
"requires_repo_info": False,
|
320 |
+
"description": "Text-to-Speech disabled"
|
321 |
+
},
|
322 |
+
{
|
323 |
+
"type": "stt",
|
324 |
+
"name": "no_stt",
|
325 |
+
"display_name": "No STT",
|
326 |
+
"requires_endpoint": False,
|
327 |
+
"requires_api_key": False,
|
328 |
+
"requires_repo_info": False,
|
329 |
+
"description": "Speech-to-Text disabled"
|
330 |
+
}
|
331 |
+
]
|
332 |
+
|
333 |
+
# ===================== Project Methods =====================
|
334 |
+
|
335 |
+
@classmethod
|
336 |
+
def get_project(cls, project_id: int) -> Optional[ProjectConfig]:
|
337 |
+
"""Get project by ID"""
|
338 |
+
config = cls.get()
|
339 |
+
return next((p for p in config.projects if p.id == project_id), None)
|
340 |
+
|
341 |
+
@classmethod
|
342 |
+
def create_project(cls, project_data: dict, username: str) -> ProjectConfig:
|
343 |
+
"""Create new project with initial version"""
|
344 |
+
with cls._lock:
|
345 |
+
config = cls.get()
|
346 |
+
|
347 |
+
# Check for duplicate name
|
348 |
+
existing_project = next((p for p in config.projects if p.name == project_data['name'] and not p.deleted), None)
|
349 |
+
if existing_project:
|
350 |
+
raise DuplicateResourceError("Project", project_data['name'])
|
351 |
+
|
352 |
+
|
353 |
+
# Create project
|
354 |
+
project = ProjectConfig(
|
355 |
+
id=config.project_id_counter,
|
356 |
+
created_date=get_current_timestamp(),
|
357 |
+
created_by=username,
|
358 |
+
version_id_counter=1, # Başlangıç değeri
|
359 |
+
versions=[], # Boş başla
|
360 |
+
**project_data
|
361 |
+
)
|
362 |
+
|
363 |
+
# Create initial version with proper models
|
364 |
+
initial_version = VersionConfig(
|
365 |
+
no=1,
|
366 |
+
caption="Initial version",
|
367 |
+
description="Auto-generated initial version",
|
368 |
+
published=False, # Explicitly set to False
|
369 |
+
deleted=False,
|
370 |
+
general_prompt="You are a helpful assistant.",
|
371 |
+
welcome_prompt=None,
|
372 |
+
llm=LLMConfiguration(
|
373 |
+
repo_id="ytu-ce-cosmos/Turkish-Llama-8b-Instruct-v0.1",
|
374 |
+
generation_config=GenerationConfig(
|
375 |
+
max_new_tokens=512,
|
376 |
+
temperature=0.7,
|
377 |
+
top_p=0.9,
|
378 |
+
repetition_penalty=1.1,
|
379 |
+
do_sample=True
|
380 |
+
),
|
381 |
+
use_fine_tune=False,
|
382 |
+
fine_tune_zip=""
|
383 |
+
),
|
384 |
+
intents=[],
|
385 |
+
created_date=get_current_timestamp(),
|
386 |
+
created_by=username,
|
387 |
+
last_update_date=None,
|
388 |
+
last_update_user=None,
|
389 |
+
publish_date=None,
|
390 |
+
published_by=None
|
391 |
+
)
|
392 |
+
|
393 |
+
# Add initial version to project
|
394 |
+
project.versions.append(initial_version)
|
395 |
+
project.version_id_counter = 2 # Next version will be 2
|
396 |
+
|
397 |
+
# Update config
|
398 |
+
config.projects.append(project)
|
399 |
+
config.project_id_counter += 1
|
400 |
+
|
401 |
+
# Log activity
|
402 |
+
cls._add_activity(
|
403 |
+
config, username, "CREATE_PROJECT",
|
404 |
+
"project", project.name,
|
405 |
+
f"Created with initial version"
|
406 |
+
)
|
407 |
+
|
408 |
+
# Save
|
409 |
+
cls.save(config, username)
|
410 |
+
|
411 |
+
log_info(
|
412 |
+
"Project created with initial version",
|
413 |
+
project_id=project.id,
|
414 |
+
name=project.name,
|
415 |
+
user=username
|
416 |
+
)
|
417 |
+
|
418 |
+
return project
|
419 |
+
|
420 |
+
@classmethod
|
421 |
+
def update_project(cls, project_id: int, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> ProjectConfig:
|
422 |
+
"""Update project with optimistic locking"""
|
423 |
+
with cls._lock:
|
424 |
+
config = cls.get()
|
425 |
+
project = cls.get_project(project_id)
|
426 |
+
|
427 |
+
if not project:
|
428 |
+
raise ResourceNotFoundError("project", project_id)
|
429 |
+
|
430 |
+
# Check race condition
|
431 |
+
if expected_last_update is not None and expected_last_update != '':
|
432 |
+
if project.last_update_date and not timestamps_equal(expected_last_update, project.last_update_date):
|
433 |
+
raise RaceConditionError(
|
434 |
+
f"Project '{project.name}' was modified by another user",
|
435 |
+
current_user=username,
|
436 |
+
last_update_user=project.last_update_user,
|
437 |
+
last_update_date=project.last_update_date,
|
438 |
+
entity_type="project",
|
439 |
+
entity_id=project_id
|
440 |
+
)
|
441 |
+
|
442 |
+
# Update fields
|
443 |
+
for key, value in update_data.items():
|
444 |
+
if hasattr(project, key) and key not in ['id', 'created_date', 'created_by', 'last_update_date', 'last_update_user']:
|
445 |
+
setattr(project, key, value)
|
446 |
+
|
447 |
+
project.last_update_date = get_current_timestamp()
|
448 |
+
project.last_update_user = username
|
449 |
+
|
450 |
+
cls._add_activity(
|
451 |
+
config, username, "UPDATE_PROJECT",
|
452 |
+
"project", project.name
|
453 |
+
)
|
454 |
+
|
455 |
+
# Save
|
456 |
+
cls.save(config, username)
|
457 |
+
|
458 |
+
log_info(
|
459 |
+
"Project updated",
|
460 |
+
project_id=project.id,
|
461 |
+
user=username
|
462 |
+
)
|
463 |
+
|
464 |
+
return project
|
465 |
+
|
466 |
+
@classmethod
|
467 |
+
def delete_project(cls, project_id: int, username: str) -> None:
|
468 |
+
"""Soft delete project"""
|
469 |
+
with cls._lock:
|
470 |
+
config = cls.get()
|
471 |
+
project = cls.get_project(project_id)
|
472 |
+
|
473 |
+
if not project:
|
474 |
+
raise ResourceNotFoundError("project", project_id)
|
475 |
+
|
476 |
+
project.deleted = True
|
477 |
+
project.last_update_date = get_current_timestamp()
|
478 |
+
project.last_update_user = username
|
479 |
+
|
480 |
+
cls._add_activity(
|
481 |
+
config, username, "DELETE_PROJECT",
|
482 |
+
"project", project.name
|
483 |
+
)
|
484 |
+
|
485 |
+
# Save
|
486 |
+
cls.save(config, username)
|
487 |
+
|
488 |
+
log_info(
|
489 |
+
"Project deleted",
|
490 |
+
project_id=project.id,
|
491 |
+
user=username
|
492 |
+
)
|
493 |
+
|
494 |
+
@classmethod
|
495 |
+
def toggle_project(cls, project_id: int, username: str) -> bool:
|
496 |
+
"""Toggle project enabled status"""
|
497 |
+
with cls._lock:
|
498 |
+
config = cls.get()
|
499 |
+
project = cls.get_project(project_id)
|
500 |
+
|
501 |
+
if not project:
|
502 |
+
raise ResourceNotFoundError("project", project_id)
|
503 |
+
|
504 |
+
project.enabled = not project.enabled
|
505 |
+
project.last_update_date = get_current_timestamp()
|
506 |
+
project.last_update_user = username
|
507 |
+
|
508 |
+
# Log activity
|
509 |
+
cls._add_activity(
|
510 |
+
config, username, "TOGGLE_PROJECT",
|
511 |
+
"project", project.name,
|
512 |
+
f"{'Enabled' if project.enabled else 'Disabled'}"
|
513 |
+
)
|
514 |
+
|
515 |
+
# Save
|
516 |
+
cls.save(config, username)
|
517 |
+
|
518 |
+
log_info(
|
519 |
+
"Project toggled",
|
520 |
+
project_id=project.id,
|
521 |
+
enabled=project.enabled,
|
522 |
+
user=username
|
523 |
+
)
|
524 |
+
|
525 |
+
return project.enabled
|
526 |
+
|
527 |
+
# ===================== Version Methods =====================
|
528 |
+
|
529 |
+
@classmethod
|
530 |
+
def create_version(cls, project_id: int, version_data: dict, username: str) -> VersionConfig:
|
531 |
+
"""Create new version"""
|
532 |
+
with cls._lock:
|
533 |
+
config = cls.get()
|
534 |
+
project = cls.get_project(project_id)
|
535 |
+
|
536 |
+
if not project:
|
537 |
+
raise ResourceNotFoundError("project", project_id)
|
538 |
+
|
539 |
+
# Handle source version copy
|
540 |
+
if 'source_version_no' in version_data and version_data['source_version_no']:
|
541 |
+
source_version = next((v for v in project.versions if v.no == version_data['source_version_no']), None)
|
542 |
+
if source_version:
|
543 |
+
# Copy from source version
|
544 |
+
version_dict = source_version.model_dump()
|
545 |
+
# Remove fields that shouldn't be copied
|
546 |
+
for field in ['no', 'created_date', 'created_by', 'published', 'publish_date',
|
547 |
+
'published_by', 'last_update_date', 'last_update_user']:
|
548 |
+
version_dict.pop(field, None)
|
549 |
+
# Override with provided data
|
550 |
+
version_dict['caption'] = version_data.get('caption', f"Copy of {source_version.caption}")
|
551 |
+
else:
|
552 |
+
# Source not found, create blank
|
553 |
+
version_dict = {
|
554 |
+
'caption': version_data.get('caption', 'New Version'),
|
555 |
+
'general_prompt': '',
|
556 |
+
'welcome_prompt': None,
|
557 |
+
'llm': {
|
558 |
+
'repo_id': '',
|
559 |
+
'generation_config': {
|
560 |
+
'max_new_tokens': 512,
|
561 |
+
'temperature': 0.7,
|
562 |
+
'top_p': 0.95,
|
563 |
+
'repetition_penalty': 1.1
|
564 |
+
},
|
565 |
+
'use_fine_tune': False,
|
566 |
+
'fine_tune_zip': ''
|
567 |
+
},
|
568 |
+
'intents': []
|
569 |
+
}
|
570 |
+
else:
|
571 |
+
# Create blank version
|
572 |
+
version_dict = {
|
573 |
+
'caption': version_data.get('caption', 'New Version'),
|
574 |
+
'general_prompt': '',
|
575 |
+
'welcome_prompt': None,
|
576 |
+
'llm': {
|
577 |
+
'repo_id': '',
|
578 |
+
'generation_config': {
|
579 |
+
'max_new_tokens': 512,
|
580 |
+
'temperature': 0.7,
|
581 |
+
'top_p': 0.95,
|
582 |
+
'repetition_penalty': 1.1
|
583 |
+
},
|
584 |
+
'use_fine_tune': False,
|
585 |
+
'fine_tune_zip': ''
|
586 |
+
},
|
587 |
+
'intents': []
|
588 |
+
}
|
589 |
+
|
590 |
+
# Create version
|
591 |
+
version = VersionConfig(
|
592 |
+
no=project.version_id_counter,
|
593 |
+
published=False, # New versions are always unpublished
|
594 |
+
deleted=False,
|
595 |
+
created_date=get_current_timestamp(),
|
596 |
+
created_by=username,
|
597 |
+
last_update_date=None,
|
598 |
+
last_update_user=None,
|
599 |
+
publish_date=None,
|
600 |
+
published_by=None,
|
601 |
+
**version_dict
|
602 |
+
)
|
603 |
+
|
604 |
+
# Update project
|
605 |
+
project.versions.append(version)
|
606 |
+
project.version_id_counter += 1
|
607 |
+
project.last_update_date = get_current_timestamp()
|
608 |
+
project.last_update_user = username
|
609 |
+
|
610 |
+
# Log activity
|
611 |
+
cls._add_activity(
|
612 |
+
config, username, "CREATE_VERSION",
|
613 |
+
"version", version.no, f"{project.name} v{version.no}",
|
614 |
+
f"Project: {project.name}"
|
615 |
+
)
|
616 |
+
|
617 |
+
# Save
|
618 |
+
cls.save(config, username)
|
619 |
+
|
620 |
+
log_info(
|
621 |
+
"Version created",
|
622 |
+
project_id=project.id,
|
623 |
+
version_no=version.no,
|
624 |
+
user=username
|
625 |
+
)
|
626 |
+
|
627 |
+
return version
|
628 |
+
|
629 |
+
@classmethod
|
630 |
+
def publish_version(cls, project_id: int, version_no: int, username: str) -> tuple[ProjectConfig, VersionConfig]:
|
631 |
+
"""Publish a version"""
|
632 |
+
with cls._lock:
|
633 |
+
config = cls.get()
|
634 |
+
project = cls.get_project(project_id)
|
635 |
+
|
636 |
+
if not project:
|
637 |
+
raise ResourceNotFoundError("project", project_id)
|
638 |
+
|
639 |
+
version = next((v for v in project.versions if v.no == version_no), None)
|
640 |
+
if not version:
|
641 |
+
raise ResourceNotFoundError("version", version_no)
|
642 |
+
|
643 |
+
# Unpublish other versions
|
644 |
+
for v in project.versions:
|
645 |
+
if v.published and v.no != version_no:
|
646 |
+
v.published = False
|
647 |
+
|
648 |
+
# Publish this version
|
649 |
+
version.published = True
|
650 |
+
version.publish_date = get_current_timestamp()
|
651 |
+
version.published_by = username
|
652 |
+
|
653 |
+
# Update project
|
654 |
+
project.last_update_date = get_current_timestamp()
|
655 |
+
project.last_update_user = username
|
656 |
+
|
657 |
+
# Log activity
|
658 |
+
cls._add_activity(
|
659 |
+
config, username, "PUBLISH_VERSION",
|
660 |
+
"version", f"{project.name} v{version.no}"
|
661 |
+
)
|
662 |
+
|
663 |
+
# Save
|
664 |
+
cls.save(config, username)
|
665 |
+
|
666 |
+
log_info(
|
667 |
+
"Version published",
|
668 |
+
project_id=project.id,
|
669 |
+
version_no=version.no,
|
670 |
+
user=username
|
671 |
+
)
|
672 |
+
|
673 |
+
return project, version
|
674 |
+
|
675 |
+
@classmethod
|
676 |
+
def update_version(cls, project_id: int, version_no: int, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> VersionConfig:
|
677 |
+
"""Update version with optimistic locking"""
|
678 |
+
with cls._lock:
|
679 |
+
config = cls.get()
|
680 |
+
project = cls.get_project(project_id)
|
681 |
+
|
682 |
+
if not project:
|
683 |
+
raise ResourceNotFoundError("project", project_id)
|
684 |
+
|
685 |
+
version = next((v for v in project.versions if v.no == version_no), None)
|
686 |
+
if not version:
|
687 |
+
raise ResourceNotFoundError("version", version_no)
|
688 |
+
|
689 |
+
# Ensure published is a boolean (safety check)
|
690 |
+
if version.published is None:
|
691 |
+
version.published = False
|
692 |
+
|
693 |
+
# Published versions cannot be edited
|
694 |
+
if version.published:
|
695 |
+
raise ValidationError("Published versions cannot be modified")
|
696 |
+
|
697 |
+
# Check race condition
|
698 |
+
if expected_last_update is not None and expected_last_update != '':
|
699 |
+
if version.last_update_date and not timestamps_equal(expected_last_update, version.last_update_date):
|
700 |
+
raise RaceConditionError(
|
701 |
+
f"Version '{version.no}' was modified by another user",
|
702 |
+
current_user=username,
|
703 |
+
last_update_user=version.last_update_user,
|
704 |
+
last_update_date=version.last_update_date,
|
705 |
+
entity_type="version",
|
706 |
+
entity_id=f"{project_id}:{version_no}"
|
707 |
+
)
|
708 |
+
|
709 |
+
# Update fields
|
710 |
+
for key, value in update_data.items():
|
711 |
+
if hasattr(version, key) and key not in ['no', 'created_date', 'created_by', 'published', 'last_update_date']:
|
712 |
+
# Handle LLM config
|
713 |
+
if key == 'llm' and isinstance(value, dict):
|
714 |
+
setattr(version, key, LLMConfiguration(**value))
|
715 |
+
# Handle intents
|
716 |
+
elif key == 'intents' and isinstance(value, list):
|
717 |
+
intents = []
|
718 |
+
for intent_data in value:
|
719 |
+
if isinstance(intent_data, dict):
|
720 |
+
intents.append(IntentConfig(**intent_data))
|
721 |
+
else:
|
722 |
+
intents.append(intent_data)
|
723 |
+
setattr(version, key, intents)
|
724 |
+
else:
|
725 |
+
setattr(version, key, value)
|
726 |
+
|
727 |
+
version.last_update_date = get_current_timestamp()
|
728 |
+
version.last_update_user = username
|
729 |
+
|
730 |
+
# Update project last update
|
731 |
+
project.last_update_date = get_current_timestamp()
|
732 |
+
project.last_update_user = username
|
733 |
+
|
734 |
+
# Log activity
|
735 |
+
cls._add_activity(
|
736 |
+
config, username, "UPDATE_VERSION",
|
737 |
+
"version", f"{project.name} v{version.no}"
|
738 |
+
)
|
739 |
+
|
740 |
+
# Save
|
741 |
+
cls.save(config, username)
|
742 |
+
|
743 |
+
log_info(
|
744 |
+
"Version updated",
|
745 |
+
project_id=project.id,
|
746 |
+
version_no=version.no,
|
747 |
+
user=username
|
748 |
+
)
|
749 |
+
|
750 |
+
return version
|
751 |
+
|
752 |
+
@classmethod
|
753 |
+
def delete_version(cls, project_id: int, version_no: int, username: str) -> None:
|
754 |
+
"""Soft delete version"""
|
755 |
+
with cls._lock:
|
756 |
+
config = cls.get()
|
757 |
+
project = cls.get_project(project_id)
|
758 |
+
|
759 |
+
if not project:
|
760 |
+
raise ResourceNotFoundError("project", project_id)
|
761 |
+
|
762 |
+
version = next((v for v in project.versions if v.no == version_no), None)
|
763 |
+
if not version:
|
764 |
+
raise ResourceNotFoundError("version", version_no)
|
765 |
+
|
766 |
+
if version.published:
|
767 |
+
raise ValidationError("Cannot delete published version")
|
768 |
+
|
769 |
+
version.deleted = True
|
770 |
+
version.last_update_date = get_current_timestamp()
|
771 |
+
version.last_update_user = username
|
772 |
+
|
773 |
+
# Update project
|
774 |
+
project.last_update_date = get_current_timestamp()
|
775 |
+
project.last_update_user = username
|
776 |
+
|
777 |
+
# Log activity
|
778 |
+
cls._add_activity(
|
779 |
+
config, username, "DELETE_VERSION",
|
780 |
+
"version", f"{project.name} v{version.no}"
|
781 |
+
)
|
782 |
+
|
783 |
+
# Save
|
784 |
+
cls.save(config, username)
|
785 |
+
|
786 |
+
log_info(
|
787 |
+
"Version deleted",
|
788 |
+
project_id=project.id,
|
789 |
+
version_no=version.no,
|
790 |
+
user=username
|
791 |
+
)
|
792 |
+
|
793 |
+
# ===================== API Methods =====================
|
794 |
+
@classmethod
|
795 |
+
def create_api(cls, api_data: dict, username: str) -> APIConfig:
|
796 |
+
"""Create new API"""
|
797 |
+
with cls._lock:
|
798 |
+
config = cls.get()
|
799 |
+
|
800 |
+
# Check for duplicate name
|
801 |
+
existing_api = next((a for a in config.apis if a.name == api_data['name'] and not a.deleted), None)
|
802 |
+
if existing_api:
|
803 |
+
raise DuplicateResourceError("API", api_data['name'])
|
804 |
+
|
805 |
+
# Create API
|
806 |
+
api = APIConfig(
|
807 |
+
created_date=get_current_timestamp(),
|
808 |
+
created_by=username,
|
809 |
+
**api_data
|
810 |
+
)
|
811 |
+
|
812 |
+
# Add to config
|
813 |
+
config.apis.append(api)
|
814 |
+
|
815 |
+
# Rebuild index
|
816 |
+
config.build_index()
|
817 |
+
|
818 |
+
# Log activity
|
819 |
+
cls._add_activity(
|
820 |
+
config, username, "CREATE_API",
|
821 |
+
"api", api.name
|
822 |
+
)
|
823 |
+
|
824 |
+
# Save
|
825 |
+
cls.save(config, username)
|
826 |
+
|
827 |
+
log_info(
|
828 |
+
"API created",
|
829 |
+
api_name=api.name,
|
830 |
+
user=username
|
831 |
+
)
|
832 |
+
|
833 |
+
return api
|
834 |
+
|
835 |
+
@classmethod
|
836 |
+
def update_api(cls, api_name: str, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> APIConfig:
|
837 |
+
"""Update API with optimistic locking"""
|
838 |
+
with cls._lock:
|
839 |
+
config = cls.get()
|
840 |
+
api = config.get_api(api_name)
|
841 |
+
|
842 |
+
if not api:
|
843 |
+
raise ResourceNotFoundError("api", api_name)
|
844 |
+
|
845 |
+
# Check race condition
|
846 |
+
if expected_last_update is not None and expected_last_update != '':
|
847 |
+
if api.last_update_date and not timestamps_equal(expected_last_update, api.last_update_date):
|
848 |
+
raise RaceConditionError(
|
849 |
+
f"API '{api.name}' was modified by another user",
|
850 |
+
current_user=username,
|
851 |
+
last_update_user=api.last_update_user,
|
852 |
+
last_update_date=api.last_update_date,
|
853 |
+
entity_type="api",
|
854 |
+
entity_id=api.name
|
855 |
+
)
|
856 |
+
|
857 |
+
# Update fields
|
858 |
+
for key, value in update_data.items():
|
859 |
+
if hasattr(api, key) and key not in ['name', 'created_date', 'created_by', 'last_update_date']:
|
860 |
+
setattr(api, key, value)
|
861 |
+
|
862 |
+
api.last_update_date = get_current_timestamp()
|
863 |
+
api.last_update_user = username
|
864 |
+
|
865 |
+
# Rebuild index
|
866 |
+
config.build_index()
|
867 |
+
|
868 |
+
# Log activity
|
869 |
+
cls._add_activity(
|
870 |
+
config, username, "UPDATE_API",
|
871 |
+
"api", api.name
|
872 |
+
)
|
873 |
+
|
874 |
+
# Save
|
875 |
+
cls.save(config, username)
|
876 |
+
|
877 |
+
log_info(
|
878 |
+
"API updated",
|
879 |
+
api_name=api.name,
|
880 |
+
user=username
|
881 |
+
)
|
882 |
+
|
883 |
+
return api
|
884 |
+
|
885 |
+
@classmethod
|
886 |
+
def delete_api(cls, api_name: str, username: str) -> None:
|
887 |
+
"""Soft delete API"""
|
888 |
+
with cls._lock:
|
889 |
+
config = cls.get()
|
890 |
+
api = config.get_api(api_name)
|
891 |
+
|
892 |
+
if not api:
|
893 |
+
raise ResourceNotFoundError("api", api_name)
|
894 |
+
|
895 |
+
api.deleted = True
|
896 |
+
api.last_update_date = get_current_timestamp()
|
897 |
+
api.last_update_user = username
|
898 |
+
|
899 |
+
# Rebuild index
|
900 |
+
config.build_index()
|
901 |
+
|
902 |
+
# Log activity
|
903 |
+
cls._add_activity(
|
904 |
+
config, username, "DELETE_API",
|
905 |
+
"api", api.name
|
906 |
+
)
|
907 |
+
|
908 |
+
# Save
|
909 |
+
cls.save(config, username)
|
910 |
+
|
911 |
+
log_info(
|
912 |
+
"API deleted",
|
913 |
+
api_name=api.name,
|
914 |
+
user=username
|
915 |
+
)
|
916 |
+
|
917 |
+
# ===================== Activity Methods =====================
|
918 |
+
@classmethod
|
919 |
+
def _add_activity(
|
920 |
+
cls,
|
921 |
+
config: ServiceConfig,
|
922 |
+
username: str,
|
923 |
+
action: str,
|
924 |
+
entity_type: str,
|
925 |
+
entity_name: Optional[str] = None,
|
926 |
+
details: Optional[str] = None
|
927 |
+
) -> None:
|
928 |
+
"""Add activity log entry"""
|
929 |
+
# Activity ID'sini oluştur - mevcut en yüksek ID'yi bul
|
930 |
+
max_id = 0
|
931 |
+
if config.activity_log:
|
932 |
+
max_id = max((entry.id for entry in config.activity_log if entry.id), default=0)
|
933 |
+
|
934 |
+
activity_id = max_id + 1
|
935 |
+
|
936 |
+
activity = ActivityLogEntry(
|
937 |
+
id=activity_id,
|
938 |
+
timestamp=get_current_timestamp(),
|
939 |
+
username=username,
|
940 |
+
action=action,
|
941 |
+
entity_type=entity_type,
|
942 |
+
entity_name=entity_name,
|
943 |
+
details=details
|
944 |
+
)
|
945 |
+
|
946 |
+
config.activity_log.append(activity)
|
947 |
+
|
948 |
+
# Keep only last 1000 entries
|
949 |
+
if len(config.activity_log) > 1000:
|
950 |
config.activity_log = config.activity_log[-1000:]
|