Spaces:
Building
Building
Upload 7 files
Browse files- utils/encrypt_string.py +28 -0
- utils/encrypt_token.py +14 -0
- utils/encryption_utils.py +88 -0
- utils/exceptions.py +193 -0
- utils/generate_key.py +18 -0
- utils/logger.py +223 -0
- utils/utils.py +162 -0
utils/encrypt_string.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse, os, sys
|
2 |
+
from cryptography.fernet import Fernet, InvalidToken
|
3 |
+
|
4 |
+
ENV = "FLARE_TOKEN_KEY"
|
5 |
+
|
6 |
+
def get_fernet(key_arg: str | None) -> Fernet:
|
7 |
+
key = key_arg or os.getenv(ENV)
|
8 |
+
if not key:
|
9 |
+
print(f"[HATA] Anahtar yok. --key parametresi verin veya {ENV} ortam değişkenini ayarlayın.", file=sys.stderr)
|
10 |
+
sys.exit(1)
|
11 |
+
try:
|
12 |
+
return Fernet(key.encode())
|
13 |
+
except Exception as e:
|
14 |
+
print(f"[HATA] Anahtar geçersiz: {e}", file=sys.stderr)
|
15 |
+
sys.exit(1)
|
16 |
+
|
17 |
+
def main():
|
18 |
+
parser = argparse.ArgumentParser(description="String şifreleyici")
|
19 |
+
parser.add_argument("plain", help="Şifrelenecek string")
|
20 |
+
parser.add_argument("--key", help="Fernet anahtarı (opsiyonel, yoksa env kullanılacak)")
|
21 |
+
args = parser.parse_args()
|
22 |
+
|
23 |
+
f = get_fernet(args.key)
|
24 |
+
enc = f.encrypt(args.plain.encode()).decode()
|
25 |
+
print(f"enc:{enc}")
|
26 |
+
|
27 |
+
if __name__ == "__main__":
|
28 |
+
main()
|
utils/encrypt_token.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
CLI: python encrypt_token.py <PLAIN_TOKEN>
|
3 |
+
Çıktıyı service_config.jsonc içindeki cloud_token alanına yapıştır.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import sys, os
|
7 |
+
from encryption_utils import encrypt
|
8 |
+
|
9 |
+
if len(sys.argv) < 2:
|
10 |
+
print("Usage: python encrypt_token.py <plain_token>")
|
11 |
+
sys.exit(1)
|
12 |
+
|
13 |
+
plain = sys.argv[1]
|
14 |
+
print(encrypt(plain))
|
utils/encryption_utils.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Flare – Fernet şifreleme yardımcıları
|
3 |
+
- encrypt(): düz string → "enc:<blob>"
|
4 |
+
- decrypt(): enc:<blob> → düz string (veya enc: yoksa aynen döner)
|
5 |
+
Anahtar: FLARE_TOKEN_KEY (32-bayt, base64, URL-safe)
|
6 |
+
"""
|
7 |
+
|
8 |
+
import os
|
9 |
+
from typing import Optional
|
10 |
+
from cryptography.fernet import Fernet, InvalidToken
|
11 |
+
from logger import log_error, log_warning
|
12 |
+
|
13 |
+
_ENV_KEY = "FLARE_TOKEN_KEY"
|
14 |
+
|
15 |
+
def _get_key() -> Fernet:
|
16 |
+
"""Get encryption key with better error messages"""
|
17 |
+
# Direkt environment variable kullan
|
18 |
+
key = os.getenv(_ENV_KEY)
|
19 |
+
|
20 |
+
# .env dosyasından yüklemeyi dene
|
21 |
+
if not key:
|
22 |
+
try:
|
23 |
+
from dotenv import load_dotenv
|
24 |
+
load_dotenv()
|
25 |
+
key = os.getenv(_ENV_KEY)
|
26 |
+
except ImportError:
|
27 |
+
pass
|
28 |
+
|
29 |
+
if not key:
|
30 |
+
error_msg = (
|
31 |
+
f"{_ENV_KEY} ortam değişkeni tanımlanmadı. "
|
32 |
+
f"Lütfen 32-byte base64 key oluşturun: python generate_key.py"
|
33 |
+
)
|
34 |
+
log_error(error_msg)
|
35 |
+
raise RuntimeError(error_msg)
|
36 |
+
|
37 |
+
# Key formatını kontrol et
|
38 |
+
try:
|
39 |
+
return Fernet(key.encode())
|
40 |
+
except Exception as e:
|
41 |
+
error_msg = (
|
42 |
+
f"{_ENV_KEY} geçersiz format. "
|
43 |
+
f"32-byte base64 URL-safe key olmalı. "
|
44 |
+
f"Yeni key için: python generate_key.py"
|
45 |
+
)
|
46 |
+
log_error(error_msg, error=str(e))
|
47 |
+
raise RuntimeError(error_msg)
|
48 |
+
|
49 |
+
def encrypt(plain: str) -> str:
|
50 |
+
"""düz string → enc:..."""
|
51 |
+
if not plain:
|
52 |
+
log_warning("Empty string passed to encrypt")
|
53 |
+
return ""
|
54 |
+
|
55 |
+
try:
|
56 |
+
f = _get_key()
|
57 |
+
encrypted = f.encrypt(plain.encode()).decode()
|
58 |
+
return "enc:" + encrypted
|
59 |
+
except Exception as e:
|
60 |
+
log_error("Encryption failed", error=str(e))
|
61 |
+
raise
|
62 |
+
|
63 |
+
def decrypt(value: Optional[str]) -> Optional[str]:
|
64 |
+
"""enc:... ise çözer, değilse aynen döndürür"""
|
65 |
+
if value is None or not isinstance(value, str):
|
66 |
+
return value
|
67 |
+
|
68 |
+
if not value.startswith("enc:"):
|
69 |
+
return value
|
70 |
+
|
71 |
+
token = value.split("enc:", 1)[1]
|
72 |
+
|
73 |
+
try:
|
74 |
+
f = _get_key()
|
75 |
+
decrypted = f.decrypt(token.encode()).decode()
|
76 |
+
return decrypted
|
77 |
+
except InvalidToken:
|
78 |
+
error_msg = (
|
79 |
+
"Şifre çözme başarısız. Muhtemel sebepler:\n"
|
80 |
+
"1. FLARE_TOKEN_KEY değişti\n"
|
81 |
+
"2. Şifreli veri bozuldu\n"
|
82 |
+
"3. Farklı bir key ile şifrelendi"
|
83 |
+
)
|
84 |
+
log_error(error_msg)
|
85 |
+
raise RuntimeError(error_msg)
|
86 |
+
except Exception as e:
|
87 |
+
log_error("Decryption error", error=str(e))
|
88 |
+
raise
|
utils/exceptions.py
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Custom Exception Classes for Flare Platform
|
3 |
+
"""
|
4 |
+
from typing import Optional, Dict, Any
|
5 |
+
from datetime import datetime
|
6 |
+
|
7 |
+
class FlareException(Exception):
|
8 |
+
"""Base exception for Flare"""
|
9 |
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
10 |
+
self.message = message
|
11 |
+
self.details = details or {}
|
12 |
+
self.timestamp = datetime.utcnow()
|
13 |
+
super().__init__(self.message)
|
14 |
+
|
15 |
+
def to_dict(self) -> Dict[str, Any]:
|
16 |
+
"""Convert exception to dictionary"""
|
17 |
+
return {
|
18 |
+
"error": self.__class__.__name__,
|
19 |
+
"message": self.message,
|
20 |
+
"details": self.details,
|
21 |
+
"timestamp": self.timestamp.isoformat()
|
22 |
+
}
|
23 |
+
|
24 |
+
def to_http_detail(self) -> Dict[str, Any]:
|
25 |
+
"""Convert to HTTP response detail"""
|
26 |
+
return {
|
27 |
+
"detail": self.message,
|
28 |
+
"error_type": self.__class__.__name__.lower().replace('error', ''),
|
29 |
+
**self.details
|
30 |
+
}
|
31 |
+
|
32 |
+
class RaceConditionError(FlareException):
|
33 |
+
"""Raised when a race condition is detected during concurrent updates"""
|
34 |
+
def __init__(
|
35 |
+
self,
|
36 |
+
message: str,
|
37 |
+
current_user: Optional[str] = None,
|
38 |
+
last_update_user: Optional[str] = None,
|
39 |
+
last_update_date: Optional[str] = None,
|
40 |
+
entity_type: Optional[str] = None,
|
41 |
+
entity_id: Optional[Any] = None
|
42 |
+
):
|
43 |
+
details = {
|
44 |
+
"current_user": current_user,
|
45 |
+
"last_update_user": last_update_user,
|
46 |
+
"last_update_date": last_update_date,
|
47 |
+
"entity_type": entity_type,
|
48 |
+
"entity_id": entity_id,
|
49 |
+
"action": "Please reload the data and try again"
|
50 |
+
}
|
51 |
+
super().__init__(message, details)
|
52 |
+
self.current_user = current_user
|
53 |
+
self.last_update_user = last_update_user
|
54 |
+
self.last_update_date = last_update_date
|
55 |
+
|
56 |
+
def to_http_detail(self) -> Dict[str, Any]:
|
57 |
+
"""Convert to HTTPException detail format with proper serialization"""
|
58 |
+
return {
|
59 |
+
"message": self.message,
|
60 |
+
"last_update_user": self.last_update_user,
|
61 |
+
"last_update_date": self.last_update_date.isoformat() if isinstance(self.last_update_date, datetime) else self.last_update_date,
|
62 |
+
"type": "race_condition"
|
63 |
+
}
|
64 |
+
|
65 |
+
class ConfigurationError(FlareException):
|
66 |
+
"""Raised when there's a configuration issue"""
|
67 |
+
def __init__(self, message: str, config_key: Optional[str] = None):
|
68 |
+
details = {"config_key": config_key} if config_key else {}
|
69 |
+
super().__init__(message, details)
|
70 |
+
|
71 |
+
class ValidationError(FlareException):
|
72 |
+
"""Raised when validation fails"""
|
73 |
+
def __init__(self, message: str, field: Optional[str] = None, value: Any = None):
|
74 |
+
details = {}
|
75 |
+
if field:
|
76 |
+
details["field"] = field
|
77 |
+
if value is not None:
|
78 |
+
details["value"] = str(value)
|
79 |
+
super().__init__(message, details)
|
80 |
+
|
81 |
+
class AuthenticationError(FlareException):
|
82 |
+
"""Raised when authentication fails"""
|
83 |
+
def __init__(self, message: str = "Authentication failed"):
|
84 |
+
super().__init__(message)
|
85 |
+
|
86 |
+
class AuthorizationError(FlareException):
|
87 |
+
"""Raised when authorization fails"""
|
88 |
+
def __init__(self, message: str = "Insufficient permissions", required_permission: Optional[str] = None):
|
89 |
+
details = {"required_permission": required_permission} if required_permission else {}
|
90 |
+
super().__init__(message, details)
|
91 |
+
|
92 |
+
class SessionError(FlareException):
|
93 |
+
"""Raised when there's a session-related error"""
|
94 |
+
def __init__(self, message: str, session_id: Optional[str] = None):
|
95 |
+
details = {"session_id": session_id} if session_id else {}
|
96 |
+
super().__init__(message, details)
|
97 |
+
|
98 |
+
class ProviderError(FlareException):
|
99 |
+
"""Raised when a provider (LLM, TTS, STT) fails"""
|
100 |
+
def __init__(self, message: str, provider_type: str, provider_name: str, original_error: Optional[str] = None):
|
101 |
+
details = {
|
102 |
+
"provider_type": provider_type,
|
103 |
+
"provider_name": provider_name,
|
104 |
+
"original_error": original_error
|
105 |
+
}
|
106 |
+
super().__init__(message, details)
|
107 |
+
|
108 |
+
class APICallError(FlareException):
|
109 |
+
"""Raised when an external API call fails"""
|
110 |
+
def __init__(
|
111 |
+
self,
|
112 |
+
message: str,
|
113 |
+
api_name: str,
|
114 |
+
status_code: Optional[int] = None,
|
115 |
+
response_body: Optional[str] = None
|
116 |
+
):
|
117 |
+
details = {
|
118 |
+
"api_name": api_name,
|
119 |
+
"status_code": status_code,
|
120 |
+
"response_body": response_body
|
121 |
+
}
|
122 |
+
super().__init__(message, details)
|
123 |
+
|
124 |
+
class WebSocketError(FlareException):
|
125 |
+
"""Raised when WebSocket operations fail"""
|
126 |
+
def __init__(self, message: str, session_id: Optional[str] = None, state: Optional[str] = None):
|
127 |
+
details = {
|
128 |
+
"session_id": session_id,
|
129 |
+
"state": state
|
130 |
+
}
|
131 |
+
super().__init__(message, details)
|
132 |
+
|
133 |
+
class ResourceNotFoundError(FlareException):
|
134 |
+
"""Raised when a requested resource is not found"""
|
135 |
+
def __init__(self, resource_type: str, resource_id: Any):
|
136 |
+
message = f"{resource_type} not found: {resource_id}"
|
137 |
+
details = {
|
138 |
+
"resource_type": resource_type,
|
139 |
+
"resource_id": str(resource_id)
|
140 |
+
}
|
141 |
+
super().__init__(message, details)
|
142 |
+
|
143 |
+
class DuplicateResourceError(FlareException):
|
144 |
+
"""Raised when attempting to create a duplicate resource"""
|
145 |
+
def __init__(self, resource_type: str, identifier: str):
|
146 |
+
message = f"{resource_type} already exists: {identifier}"
|
147 |
+
details = {
|
148 |
+
"resource_type": resource_type,
|
149 |
+
"identifier": identifier
|
150 |
+
}
|
151 |
+
super().__init__(message, details)
|
152 |
+
|
153 |
+
# Error response formatters
|
154 |
+
def format_error_response(error: Exception, request_id: Optional[str] = None) -> Dict[str, Any]:
|
155 |
+
"""Format any exception into a standardized error response"""
|
156 |
+
if isinstance(error, FlareException):
|
157 |
+
response = error.to_dict()
|
158 |
+
else:
|
159 |
+
# Generic error
|
160 |
+
response = {
|
161 |
+
"error": error.__class__.__name__,
|
162 |
+
"message": str(error),
|
163 |
+
"details": {},
|
164 |
+
"timestamp": datetime.utcnow().isoformat()
|
165 |
+
}
|
166 |
+
|
167 |
+
if request_id:
|
168 |
+
response["request_id"] = request_id
|
169 |
+
|
170 |
+
return response
|
171 |
+
|
172 |
+
def get_http_status_code(error: Exception) -> int:
|
173 |
+
"""Get appropriate HTTP status code for an exception"""
|
174 |
+
status_map = {
|
175 |
+
ValidationError: 422,
|
176 |
+
AuthenticationError: 401,
|
177 |
+
AuthorizationError: 403,
|
178 |
+
ResourceNotFoundError: 404,
|
179 |
+
DuplicateResourceError: 409,
|
180 |
+
RaceConditionError: 409,
|
181 |
+
ConfigurationError: 500,
|
182 |
+
ProviderError: 503,
|
183 |
+
APICallError: 502,
|
184 |
+
WebSocketError: 500,
|
185 |
+
SessionError: 400
|
186 |
+
}
|
187 |
+
|
188 |
+
for error_class, status_code in status_map.items():
|
189 |
+
if isinstance(error, error_class):
|
190 |
+
return status_code
|
191 |
+
|
192 |
+
# Default to 500 for unknown errors
|
193 |
+
return 500
|
utils/generate_key.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
generate_key.py
|
4 |
+
----------------------------------
|
5 |
+
Çalıştır: python generate_key.py
|
6 |
+
Çıktı: 8rSihw0d3Sh_ceyDYobMHNPrgSg0riwGSK4Vco3O5qA=
|
7 |
+
Bu çıktıyı ortam değişkeni (veya HF Spaces `Secrets`) olarak kullan.
|
8 |
+
|
9 |
+
Not: cryptography==42.x yüklü olmalı (requirements.txt’de varsa yeterli).
|
10 |
+
"""
|
11 |
+
from cryptography.fernet import Fernet
|
12 |
+
|
13 |
+
def main():
|
14 |
+
key = Fernet.generate_key().decode()
|
15 |
+
print(key)
|
16 |
+
|
17 |
+
if __name__ == "__main__":
|
18 |
+
main()
|
utils/logger.py
ADDED
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Centralized Logging System for Flare Platform
|
3 |
+
"""
|
4 |
+
import sys
|
5 |
+
import logging
|
6 |
+
import json
|
7 |
+
import os
|
8 |
+
import threading
|
9 |
+
import traceback
|
10 |
+
from datetime import datetime
|
11 |
+
from enum import Enum
|
12 |
+
from typing import Optional, Dict, Any, Union
|
13 |
+
from pathlib import Path
|
14 |
+
|
15 |
+
class LogLevel(Enum):
|
16 |
+
DEBUG = "DEBUG"
|
17 |
+
INFO = "INFO"
|
18 |
+
WARNING = "WARNING"
|
19 |
+
ERROR = "ERROR"
|
20 |
+
CRITICAL = "CRITICAL"
|
21 |
+
|
22 |
+
class FlareLogger:
|
23 |
+
_instance = None
|
24 |
+
_lock = threading.Lock()
|
25 |
+
|
26 |
+
def __new__(cls):
|
27 |
+
if cls._instance is None:
|
28 |
+
with cls._lock:
|
29 |
+
if cls._instance is None:
|
30 |
+
cls._instance = super().__new__(cls)
|
31 |
+
cls._instance._initialized = False
|
32 |
+
return cls._instance
|
33 |
+
|
34 |
+
def __init__(self):
|
35 |
+
if self._initialized:
|
36 |
+
return
|
37 |
+
|
38 |
+
self._initialized = True
|
39 |
+
|
40 |
+
# Log level from environment
|
41 |
+
self.log_level = LogLevel[os.getenv('LOG_LEVEL', 'INFO')]
|
42 |
+
|
43 |
+
# Configure Python logging
|
44 |
+
self.logger = logging.getLogger('flare')
|
45 |
+
self.logger.setLevel(self.log_level.value)
|
46 |
+
|
47 |
+
# Remove default handlers
|
48 |
+
self.logger.handlers = []
|
49 |
+
|
50 |
+
# Console handler with custom format
|
51 |
+
console_handler = logging.StreamHandler(sys.stdout)
|
52 |
+
console_handler.setFormatter(self._get_formatter())
|
53 |
+
self.logger.addHandler(console_handler)
|
54 |
+
|
55 |
+
# File handler for production
|
56 |
+
if os.getenv('LOG_TO_FILE', 'false').lower() == 'true':
|
57 |
+
log_dir = Path('logs')
|
58 |
+
log_dir.mkdir(exist_ok=True)
|
59 |
+
file_handler = logging.FileHandler(
|
60 |
+
log_dir / f"flare_{datetime.now().strftime('%Y%m%d')}.log"
|
61 |
+
)
|
62 |
+
file_handler.setFormatter(self._get_formatter())
|
63 |
+
self.logger.addHandler(file_handler)
|
64 |
+
|
65 |
+
# Future: Add ElasticSearch handler here
|
66 |
+
# if os.getenv('ELASTICSEARCH_URL'):
|
67 |
+
# from elasticsearch_handler import ElasticsearchHandler
|
68 |
+
# es_handler = ElasticsearchHandler(
|
69 |
+
# hosts=[os.getenv('ELASTICSEARCH_URL')],
|
70 |
+
# index='flare-logs'
|
71 |
+
# )
|
72 |
+
# self.logger.addHandler(es_handler)
|
73 |
+
|
74 |
+
def _get_formatter(self):
|
75 |
+
return logging.Formatter(
|
76 |
+
'[%(asctime)s.%(msecs)03d] [%(levelname)s] [%(name)s] %(message)s',
|
77 |
+
datefmt='%H:%M:%S'
|
78 |
+
)
|
79 |
+
|
80 |
+
def log(self, level: LogLevel, message: str, **kwargs):
|
81 |
+
"""Central logging method with structured data"""
|
82 |
+
# Add context data
|
83 |
+
extra_data = {
|
84 |
+
'timestamp': datetime.utcnow().isoformat(),
|
85 |
+
'service': 'flare',
|
86 |
+
'thread_id': threading.get_ident(),
|
87 |
+
**kwargs
|
88 |
+
}
|
89 |
+
|
90 |
+
# Log with structured data
|
91 |
+
log_message = message
|
92 |
+
if kwargs:
|
93 |
+
# Format kwargs for readability
|
94 |
+
kwargs_str = json.dumps(kwargs, ensure_ascii=False, default=str)
|
95 |
+
log_message = f"{message} | {kwargs_str}"
|
96 |
+
|
97 |
+
getattr(self.logger, level.value.lower())(log_message, extra={'data': extra_data})
|
98 |
+
|
99 |
+
# Always flush for real-time debugging
|
100 |
+
sys.stdout.flush()
|
101 |
+
|
102 |
+
def debug(self, message: str, **kwargs):
|
103 |
+
"""Log debug message"""
|
104 |
+
self.log(LogLevel.DEBUG, message, **kwargs)
|
105 |
+
|
106 |
+
def info(self, message: str, **kwargs):
|
107 |
+
"""Log info message"""
|
108 |
+
self.log(LogLevel.INFO, message, **kwargs)
|
109 |
+
|
110 |
+
def warning(self, message: str, **kwargs):
|
111 |
+
"""Log warning message"""
|
112 |
+
self.log(LogLevel.WARNING, message, **kwargs)
|
113 |
+
|
114 |
+
def error(self, message: str, **kwargs):
|
115 |
+
"""Log error message"""
|
116 |
+
self.log(LogLevel.ERROR, message, **kwargs)
|
117 |
+
|
118 |
+
def critical(self, message: str, **kwargs):
|
119 |
+
"""Log critical message"""
|
120 |
+
self.log(LogLevel.CRITICAL, message, **kwargs)
|
121 |
+
|
122 |
+
def set_level(self, level: str):
|
123 |
+
"""Dynamically change log level"""
|
124 |
+
try:
|
125 |
+
self.log_level = LogLevel[level.upper()]
|
126 |
+
self.logger.setLevel(self.log_level.value)
|
127 |
+
self.info(f"Log level changed to {level}")
|
128 |
+
except KeyError:
|
129 |
+
self.warning(f"Invalid log level: {level}")
|
130 |
+
|
131 |
+
# Global logger instance
|
132 |
+
logger = FlareLogger()
|
133 |
+
|
134 |
+
# Convenience functions
|
135 |
+
def log_debug(message: str, **kwargs):
|
136 |
+
"""Log debug message"""
|
137 |
+
logger.debug(message, **kwargs)
|
138 |
+
|
139 |
+
def log_info(message: str, **kwargs):
|
140 |
+
"""Log info message"""
|
141 |
+
logger.info(message, **kwargs)
|
142 |
+
|
143 |
+
def log_warning(message: str, **kwargs):
|
144 |
+
"""Log warning message"""
|
145 |
+
logger.warning(message, **kwargs)
|
146 |
+
|
147 |
+
def log_error(message: str, exception: Optional[Exception] = None, **kwargs):
|
148 |
+
"""
|
149 |
+
Log error message with optional exception
|
150 |
+
|
151 |
+
Usage:
|
152 |
+
log_error("Error occurred")
|
153 |
+
log_error("Error occurred", e) # Otomatik olarak str(e) ve traceback ekler
|
154 |
+
log_error("Error occurred", error="custom error")
|
155 |
+
log_error("Error occurred", e, extra_field="value")
|
156 |
+
"""
|
157 |
+
import traceback
|
158 |
+
|
159 |
+
# Eğer exception parametresi verilmişse, otomatik olarak error ve traceback ekle
|
160 |
+
if exception is not None:
|
161 |
+
# Eğer kwargs'da error yoksa, exception'dan al
|
162 |
+
if 'error' not in kwargs:
|
163 |
+
kwargs['error'] = str(exception)
|
164 |
+
|
165 |
+
# Exception tipini ekle
|
166 |
+
if 'error_type' not in kwargs:
|
167 |
+
kwargs['error_type'] = type(exception).__name__
|
168 |
+
|
169 |
+
# Eğer kwargs'da traceback yoksa ve bu bir Exception ise, traceback ekle
|
170 |
+
if 'traceback' not in kwargs and isinstance(exception, Exception):
|
171 |
+
kwargs['traceback'] = traceback.format_exc()
|
172 |
+
|
173 |
+
# Özel exception tipleri için ekstra bilgi
|
174 |
+
if hasattr(exception, '__dict__'):
|
175 |
+
# Custom exception'ların attribute'larını ekle
|
176 |
+
for attr, value in exception.__dict__.items():
|
177 |
+
if not attr.startswith('_') and attr not in kwargs:
|
178 |
+
kwargs[f'exc_{attr}'] = value
|
179 |
+
|
180 |
+
# HTTP status code varsa ekle
|
181 |
+
if hasattr(exception, 'status_code') and 'status_code' not in kwargs:
|
182 |
+
kwargs['status_code'] = exception.status_code
|
183 |
+
|
184 |
+
# Orijinal logger'a gönder
|
185 |
+
logger.error(message, **kwargs)
|
186 |
+
|
187 |
+
def log_critical(message: str, **kwargs):
|
188 |
+
"""Log critical message"""
|
189 |
+
logger.critical(message, **kwargs)
|
190 |
+
|
191 |
+
# Backward compatibility
|
192 |
+
def log(message: str, level: str = "INFO", **kwargs):
|
193 |
+
"""Legacy log function for compatibility"""
|
194 |
+
getattr(logger, level.lower())(message, **kwargs)
|
195 |
+
|
196 |
+
# Performance logging helpers
|
197 |
+
class LogTimer:
|
198 |
+
"""Context manager for timing operations"""
|
199 |
+
def __init__(self, operation_name: str, **extra_kwargs):
|
200 |
+
self.operation_name = operation_name
|
201 |
+
self.extra_kwargs = extra_kwargs
|
202 |
+
self.start_time = None
|
203 |
+
|
204 |
+
def __enter__(self):
|
205 |
+
self.start_time = datetime.now()
|
206 |
+
log_debug(f"Starting {self.operation_name}", **self.extra_kwargs)
|
207 |
+
return self
|
208 |
+
|
209 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
210 |
+
duration_ms = (datetime.now() - self.start_time).total_seconds() * 1000
|
211 |
+
if exc_type:
|
212 |
+
log_error(
|
213 |
+
f"{self.operation_name} failed after {duration_ms:.2f}ms",
|
214 |
+
error=str(exc_val),
|
215 |
+
duration_ms=duration_ms,
|
216 |
+
**self.extra_kwargs
|
217 |
+
)
|
218 |
+
else:
|
219 |
+
log_info(
|
220 |
+
f"{self.operation_name} completed in {duration_ms:.2f}ms",
|
221 |
+
duration_ms=duration_ms,
|
222 |
+
**self.extra_kwargs
|
223 |
+
)
|
utils/utils.py
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import os
|
3 |
+
from typing import Optional
|
4 |
+
from fastapi import HTTPException, Depends
|
5 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
6 |
+
from datetime import datetime, timedelta, timezone
|
7 |
+
import jwt
|
8 |
+
from logger import log_info, log_warning
|
9 |
+
|
10 |
+
security = HTTPBearer()
|
11 |
+
|
12 |
+
# ===================== Rate Limiting =====================
|
13 |
+
class RateLimiter:
|
14 |
+
"""Simple in-memory rate limiter"""
|
15 |
+
def __init__(self):
|
16 |
+
self.requests = {} # {key: [(timestamp, count)]}
|
17 |
+
self.lock = threading.Lock()
|
18 |
+
|
19 |
+
def is_allowed(self, key: str, max_requests: int, window_seconds: int) -> bool:
|
20 |
+
"""Check if request is allowed"""
|
21 |
+
with self.lock:
|
22 |
+
now = datetime.now(timezone.utc)
|
23 |
+
|
24 |
+
if key not in self.requests:
|
25 |
+
self.requests[key] = []
|
26 |
+
|
27 |
+
# Remove old entries
|
28 |
+
cutoff = now.timestamp() - window_seconds
|
29 |
+
self.requests[key] = [
|
30 |
+
(ts, count) for ts, count in self.requests[key]
|
31 |
+
if ts > cutoff
|
32 |
+
]
|
33 |
+
|
34 |
+
# Count requests in window
|
35 |
+
total = sum(count for _, count in self.requests[key])
|
36 |
+
|
37 |
+
if total >= max_requests:
|
38 |
+
return False
|
39 |
+
|
40 |
+
# Add this request
|
41 |
+
self.requests[key].append((now.timestamp(), 1))
|
42 |
+
return True
|
43 |
+
|
44 |
+
def reset(self, key: str):
|
45 |
+
"""Reset rate limit for key"""
|
46 |
+
with self.lock:
|
47 |
+
if key in self.requests:
|
48 |
+
del self.requests[key]
|
49 |
+
|
50 |
+
# Create global rate limiter instance
|
51 |
+
import threading
|
52 |
+
rate_limiter = RateLimiter()
|
53 |
+
|
54 |
+
# ===================== JWT Config =====================
|
55 |
+
def get_jwt_config():
|
56 |
+
"""Get JWT configuration based on environment"""
|
57 |
+
# Check if we're in HuggingFace Space
|
58 |
+
if os.getenv("SPACE_ID"):
|
59 |
+
# Cloud mode - use secrets from environment
|
60 |
+
jwt_secret = os.getenv("JWT_SECRET")
|
61 |
+
if not jwt_secret:
|
62 |
+
log_warning("⚠️ WARNING: JWT_SECRET not found in environment, using fallback")
|
63 |
+
jwt_secret = "flare-admin-secret-key-change-in-production" # Fallback
|
64 |
+
else:
|
65 |
+
# On-premise mode - use .env file
|
66 |
+
from dotenv import load_dotenv
|
67 |
+
load_dotenv()
|
68 |
+
jwt_secret = os.getenv("JWT_SECRET", "flare-admin-secret-key-change-in-production")
|
69 |
+
|
70 |
+
return {
|
71 |
+
"secret": jwt_secret,
|
72 |
+
"algorithm": os.getenv("JWT_ALGORITHM", "HS256"),
|
73 |
+
"expiration_hours": int(os.getenv("JWT_EXPIRATION_HOURS", "24"))
|
74 |
+
}
|
75 |
+
|
76 |
+
# ===================== Auth Helpers =====================
|
77 |
+
def create_token(username: str) -> str:
|
78 |
+
"""Create JWT token for user"""
|
79 |
+
config = get_jwt_config()
|
80 |
+
expiry = datetime.now(timezone.utc) + timedelta(hours=config["expiration_hours"])
|
81 |
+
|
82 |
+
payload = {
|
83 |
+
"sub": username,
|
84 |
+
"exp": expiry,
|
85 |
+
"iat": datetime.now(timezone.utc)
|
86 |
+
}
|
87 |
+
|
88 |
+
return jwt.encode(payload, config["secret"], algorithm=config["algorithm"])
|
89 |
+
|
90 |
+
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
|
91 |
+
"""Verify JWT token and return username"""
|
92 |
+
token = credentials.credentials
|
93 |
+
config = get_jwt_config()
|
94 |
+
|
95 |
+
try:
|
96 |
+
payload = jwt.decode(token, config["secret"], algorithms=[config["algorithm"]])
|
97 |
+
return payload["sub"]
|
98 |
+
except jwt.ExpiredSignatureError:
|
99 |
+
raise HTTPException(status_code=401, detail="Token expired")
|
100 |
+
except jwt.InvalidTokenError:
|
101 |
+
raise HTTPException(status_code=401, detail="Invalid token")
|
102 |
+
|
103 |
+
# ===================== Utility Functions =====================
|
104 |
+
|
105 |
+
def truncate_string(text: str, max_length: int = 100, suffix: str = "...") -> str:
|
106 |
+
"""Truncate string to max length"""
|
107 |
+
if len(text) <= max_length:
|
108 |
+
return text
|
109 |
+
return text[:max_length - len(suffix)] + suffix
|
110 |
+
|
111 |
+
def format_file_size(size_bytes: int) -> str:
|
112 |
+
"""Format file size in human readable format"""
|
113 |
+
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
114 |
+
if size_bytes < 1024.0:
|
115 |
+
return f"{size_bytes:.2f} {unit}"
|
116 |
+
size_bytes /= 1024.0
|
117 |
+
return f"{size_bytes:.2f} PB"
|
118 |
+
|
119 |
+
def is_safe_path(path: str, base_path: str) -> bool:
|
120 |
+
"""Check if path is safe (no directory traversal)"""
|
121 |
+
import os
|
122 |
+
# Resolve to absolute paths
|
123 |
+
base = os.path.abspath(base_path)
|
124 |
+
target = os.path.abspath(os.path.join(base, path))
|
125 |
+
|
126 |
+
# Check if target is under base
|
127 |
+
return target.startswith(base)
|
128 |
+
|
129 |
+
def get_current_timestamp() -> str:
|
130 |
+
"""
|
131 |
+
Get current UTC timestamp in ISO format with Z suffix
|
132 |
+
Returns: "2025-01-10T12:00:00.123Z"
|
133 |
+
"""
|
134 |
+
return datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
|
135 |
+
|
136 |
+
def normalize_timestamp(timestamp: Optional[str]) -> str:
|
137 |
+
"""
|
138 |
+
Normalize timestamp string for consistent comparison
|
139 |
+
Handles various formats:
|
140 |
+
- "2025-01-10T12:00:00Z"
|
141 |
+
- "2025-01-10T12:00:00.000Z"
|
142 |
+
- "2025-01-10T12:00:00+00:00"
|
143 |
+
- "2025-01-10 12:00:00+00:00"
|
144 |
+
"""
|
145 |
+
if not timestamp:
|
146 |
+
return ""
|
147 |
+
|
148 |
+
# Normalize various formats
|
149 |
+
normalized = timestamp.replace(' ', 'T') # Space to T
|
150 |
+
normalized = normalized.replace('+00:00', 'Z') # UTC timezone
|
151 |
+
|
152 |
+
# Remove milliseconds if present for comparison
|
153 |
+
if '.' in normalized and normalized.endswith('Z'):
|
154 |
+
normalized = normalized.split('.')[0] + 'Z'
|
155 |
+
|
156 |
+
return normalized
|
157 |
+
|
158 |
+
def timestamps_equal(ts1: Optional[str], ts2: Optional[str]) -> bool:
|
159 |
+
"""
|
160 |
+
Compare two timestamps regardless of format differences
|
161 |
+
"""
|
162 |
+
return normalize_timestamp(ts1) == normalize_timestamp(ts2)
|