nedo-vision-worker 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nedo_vision_worker/__init__.py +10 -0
- nedo_vision_worker/cli.py +195 -0
- nedo_vision_worker/config/ConfigurationManager.py +196 -0
- nedo_vision_worker/config/__init__.py +1 -0
- nedo_vision_worker/database/DatabaseManager.py +219 -0
- nedo_vision_worker/database/__init__.py +1 -0
- nedo_vision_worker/doctor.py +453 -0
- nedo_vision_worker/initializer/AppInitializer.py +78 -0
- nedo_vision_worker/initializer/__init__.py +1 -0
- nedo_vision_worker/models/__init__.py +15 -0
- nedo_vision_worker/models/ai_model.py +29 -0
- nedo_vision_worker/models/auth.py +14 -0
- nedo_vision_worker/models/config.py +9 -0
- nedo_vision_worker/models/dataset_source.py +30 -0
- nedo_vision_worker/models/logs.py +9 -0
- nedo_vision_worker/models/ppe_detection.py +39 -0
- nedo_vision_worker/models/ppe_detection_label.py +20 -0
- nedo_vision_worker/models/restricted_area_violation.py +20 -0
- nedo_vision_worker/models/user.py +10 -0
- nedo_vision_worker/models/worker_source.py +19 -0
- nedo_vision_worker/models/worker_source_pipeline.py +21 -0
- nedo_vision_worker/models/worker_source_pipeline_config.py +24 -0
- nedo_vision_worker/models/worker_source_pipeline_debug.py +15 -0
- nedo_vision_worker/models/worker_source_pipeline_detection.py +14 -0
- nedo_vision_worker/protos/AIModelService_pb2.py +46 -0
- nedo_vision_worker/protos/AIModelService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/DatasetSourceService_pb2.py +46 -0
- nedo_vision_worker/protos/DatasetSourceService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/HumanDetectionService_pb2.py +44 -0
- nedo_vision_worker/protos/HumanDetectionService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/PPEDetectionService_pb2.py +46 -0
- nedo_vision_worker/protos/PPEDetectionService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/VisionWorkerService_pb2.py +72 -0
- nedo_vision_worker/protos/VisionWorkerService_pb2_grpc.py +471 -0
- nedo_vision_worker/protos/WorkerSourcePipelineService_pb2.py +64 -0
- nedo_vision_worker/protos/WorkerSourcePipelineService_pb2_grpc.py +312 -0
- nedo_vision_worker/protos/WorkerSourceService_pb2.py +50 -0
- nedo_vision_worker/protos/WorkerSourceService_pb2_grpc.py +183 -0
- nedo_vision_worker/protos/__init__.py +1 -0
- nedo_vision_worker/repositories/AIModelRepository.py +44 -0
- nedo_vision_worker/repositories/DatasetSourceRepository.py +150 -0
- nedo_vision_worker/repositories/PPEDetectionRepository.py +112 -0
- nedo_vision_worker/repositories/RestrictedAreaRepository.py +88 -0
- nedo_vision_worker/repositories/WorkerSourcePipelineDebugRepository.py +90 -0
- nedo_vision_worker/repositories/WorkerSourcePipelineDetectionRepository.py +48 -0
- nedo_vision_worker/repositories/WorkerSourcePipelineRepository.py +174 -0
- nedo_vision_worker/repositories/WorkerSourceRepository.py +46 -0
- nedo_vision_worker/repositories/__init__.py +1 -0
- nedo_vision_worker/services/AIModelClient.py +362 -0
- nedo_vision_worker/services/ConnectionInfoClient.py +57 -0
- nedo_vision_worker/services/DatasetSourceClient.py +88 -0
- nedo_vision_worker/services/FileToRTMPServer.py +78 -0
- nedo_vision_worker/services/GrpcClientBase.py +155 -0
- nedo_vision_worker/services/GrpcClientManager.py +141 -0
- nedo_vision_worker/services/ImageUploadClient.py +82 -0
- nedo_vision_worker/services/PPEDetectionClient.py +108 -0
- nedo_vision_worker/services/RTSPtoRTMPStreamer.py +98 -0
- nedo_vision_worker/services/RestrictedAreaClient.py +100 -0
- nedo_vision_worker/services/SystemUsageClient.py +77 -0
- nedo_vision_worker/services/VideoStreamClient.py +161 -0
- nedo_vision_worker/services/WorkerSourceClient.py +215 -0
- nedo_vision_worker/services/WorkerSourcePipelineClient.py +393 -0
- nedo_vision_worker/services/WorkerSourceUpdater.py +134 -0
- nedo_vision_worker/services/WorkerStatusClient.py +65 -0
- nedo_vision_worker/services/__init__.py +1 -0
- nedo_vision_worker/util/HardwareID.py +104 -0
- nedo_vision_worker/util/ImageUploader.py +92 -0
- nedo_vision_worker/util/Networking.py +94 -0
- nedo_vision_worker/util/PlatformDetector.py +50 -0
- nedo_vision_worker/util/SystemMonitor.py +299 -0
- nedo_vision_worker/util/VideoProbeUtil.py +120 -0
- nedo_vision_worker/util/__init__.py +1 -0
- nedo_vision_worker/worker/CoreActionWorker.py +125 -0
- nedo_vision_worker/worker/DataSenderWorker.py +168 -0
- nedo_vision_worker/worker/DataSyncWorker.py +143 -0
- nedo_vision_worker/worker/DatasetFrameSender.py +208 -0
- nedo_vision_worker/worker/DatasetFrameWorker.py +412 -0
- nedo_vision_worker/worker/PPEDetectionManager.py +86 -0
- nedo_vision_worker/worker/PipelineActionWorker.py +129 -0
- nedo_vision_worker/worker/PipelineImageWorker.py +116 -0
- nedo_vision_worker/worker/RabbitMQListener.py +170 -0
- nedo_vision_worker/worker/RestrictedAreaManager.py +85 -0
- nedo_vision_worker/worker/SystemUsageManager.py +111 -0
- nedo_vision_worker/worker/VideoStreamWorker.py +139 -0
- nedo_vision_worker/worker/WorkerManager.py +155 -0
- nedo_vision_worker/worker/__init__.py +1 -0
- nedo_vision_worker/worker_service.py +264 -0
- nedo_vision_worker-1.0.0.dist-info/METADATA +563 -0
- nedo_vision_worker-1.0.0.dist-info/RECORD +92 -0
- nedo_vision_worker-1.0.0.dist-info/WHEEL +5 -0
- nedo_vision_worker-1.0.0.dist-info/entry_points.txt +2 -0
- nedo_vision_worker-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from .worker_service import WorkerService
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def signal_handler(signum, frame):
|
|
11
|
+
"""Handle system signals for graceful shutdown"""
|
|
12
|
+
logging.info(f"Received signal {signum}, shutting down...")
|
|
13
|
+
sys.exit(0)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
"""Main CLI entry point."""
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
description="Nedo Vision Worker Service Library CLI",
|
|
20
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
21
|
+
epilog="""
|
|
22
|
+
Examples:
|
|
23
|
+
# Check system dependencies and requirements
|
|
24
|
+
nedo-worker doctor
|
|
25
|
+
|
|
26
|
+
# Start worker service with required parameters
|
|
27
|
+
nedo-worker run --token your-token-here --rtmp-server rtmp://server.com:1935/live
|
|
28
|
+
|
|
29
|
+
# Start with custom server host
|
|
30
|
+
nedo-worker run --token your-token-here --rtmp-server rtmp://server.com:1935/live --server-host custom.server.com
|
|
31
|
+
|
|
32
|
+
# Start with custom storage path
|
|
33
|
+
nedo-worker run --token your-token-here --rtmp-server rtmp://server.com:1935/live --storage-path /path/to/storage
|
|
34
|
+
"""
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Add subcommands
|
|
38
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
|
39
|
+
|
|
40
|
+
# Doctor command
|
|
41
|
+
doctor_parser = subparsers.add_parser(
|
|
42
|
+
'doctor',
|
|
43
|
+
help='Check system dependencies and requirements',
|
|
44
|
+
description='Run diagnostic checks for FFmpeg, OpenCV, gRPC and other dependencies'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Run command
|
|
48
|
+
run_parser = subparsers.add_parser(
|
|
49
|
+
'run',
|
|
50
|
+
help='Start the worker service',
|
|
51
|
+
description='Start the Nedo Vision Worker Service'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
run_parser.add_argument(
|
|
55
|
+
"--server-host",
|
|
56
|
+
default="be.vision.sindika.co.id",
|
|
57
|
+
help="Server hostname for communication (default: be.vision.sindika.co.id)"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
run_parser.add_argument(
|
|
61
|
+
"--token",
|
|
62
|
+
required=True,
|
|
63
|
+
help="Authentication token for the worker (obtained from frontend)"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
run_parser.add_argument(
|
|
67
|
+
"--system-usage-interval",
|
|
68
|
+
type=int,
|
|
69
|
+
default=30,
|
|
70
|
+
help="System usage reporting interval in seconds (default: 30)"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
run_parser.add_argument(
|
|
74
|
+
"--rtmp-server",
|
|
75
|
+
required=True,
|
|
76
|
+
help="RTMP server URL for video streaming (e.g., rtmp://server.com:1935/live)"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
run_parser.add_argument(
|
|
80
|
+
"--storage-path",
|
|
81
|
+
default="data",
|
|
82
|
+
help="Storage path for databases and files (default: data)"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Add legacy arguments for backward compatibility (when no subcommand is used)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--token",
|
|
88
|
+
help="(Legacy) Authentication token for the worker (obtained from frontend)"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--server-host",
|
|
93
|
+
default="be.vision.sindika.co.id",
|
|
94
|
+
help="(Legacy) Server hostname for communication (default: be.vision.sindika.co.id)"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--system-usage-interval",
|
|
99
|
+
type=int,
|
|
100
|
+
default=30,
|
|
101
|
+
help="(Legacy) System usage reporting interval in seconds (default: 30)"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--rtmp-server",
|
|
106
|
+
help="(Legacy) RTMP server URL for video streaming (e.g., rtmp://server.com:1935/live)"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--storage-path",
|
|
111
|
+
default="data",
|
|
112
|
+
help="(Legacy) Storage path for databases and files (default: data)"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--version",
|
|
117
|
+
action="version",
|
|
118
|
+
version="nedo-vision-worker 1.0.0"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--doctor",
|
|
123
|
+
action="store_true",
|
|
124
|
+
help="(Deprecated) Run system diagnostics - use 'nedo-worker doctor' instead"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
# Handle subcommands
|
|
130
|
+
if args.command == 'doctor':
|
|
131
|
+
from .doctor import main as doctor_main
|
|
132
|
+
sys.exit(doctor_main())
|
|
133
|
+
elif args.command == 'run':
|
|
134
|
+
run_worker_service(args)
|
|
135
|
+
elif args.doctor: # Legacy mode - deprecated --doctor flag
|
|
136
|
+
print("⚠️ Warning: Using deprecated --doctor flag. Use 'nedo-worker doctor' instead.")
|
|
137
|
+
from .doctor import main as doctor_main
|
|
138
|
+
sys.exit(doctor_main())
|
|
139
|
+
elif args.token and args.rtmp_server: # Legacy mode - if token and rtmp_server are provided without subcommand
|
|
140
|
+
print("⚠️ Warning: Using legacy command format. Consider using 'nedo-worker run --token ... --rtmp-server ...' instead.")
|
|
141
|
+
run_worker_service(args)
|
|
142
|
+
else:
|
|
143
|
+
# If no subcommand provided and no token, show help
|
|
144
|
+
parser.print_help()
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def run_worker_service(args):
|
|
149
|
+
"""Run the worker service with the provided arguments."""
|
|
150
|
+
# Set up signal handlers for graceful shutdown
|
|
151
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
152
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
153
|
+
|
|
154
|
+
logger = logging.getLogger(__name__)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# Create and start the worker service
|
|
158
|
+
service = WorkerService(
|
|
159
|
+
server_host=args.server_host,
|
|
160
|
+
token=args.token,
|
|
161
|
+
system_usage_interval=args.system_usage_interval,
|
|
162
|
+
rtmp_server=args.rtmp_server,
|
|
163
|
+
storage_path=args.storage_path
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
logger.info("🚀 Starting Nedo Vision Worker Service...")
|
|
167
|
+
logger.info(f"🌐 Server: {args.server_host}")
|
|
168
|
+
logger.info(f"🔑 Token: {args.token[:8]}...")
|
|
169
|
+
logger.info(f"⏱️ System Usage Interval: {args.system_usage_interval}s")
|
|
170
|
+
logger.info(f"📡 RTMP Server: {args.rtmp_server}")
|
|
171
|
+
logger.info(f"💾 Storage Path: {args.storage_path}")
|
|
172
|
+
logger.info("Press Ctrl+C to stop the service")
|
|
173
|
+
|
|
174
|
+
# Start the service
|
|
175
|
+
service.run()
|
|
176
|
+
|
|
177
|
+
# Keep the service running
|
|
178
|
+
try:
|
|
179
|
+
while getattr(service, 'running', False):
|
|
180
|
+
import time
|
|
181
|
+
time.sleep(1)
|
|
182
|
+
except KeyboardInterrupt:
|
|
183
|
+
logger.info("\n🛑 Shutdown requested...")
|
|
184
|
+
finally:
|
|
185
|
+
service.stop()
|
|
186
|
+
logger.info("✅ Service stopped successfully")
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"❌ Error: {e}")
|
|
190
|
+
traceback.print_exc()
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
main()
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from ..models.config import ConfigEntity # ORM model for server_config
|
|
3
|
+
from ..database.DatabaseManager import DatabaseManager # DatabaseManager for managing sessions
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConfigurationManager:
|
|
7
|
+
"""
|
|
8
|
+
A class to manage server configuration stored in the 'config' database using SQLAlchemy.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def init_database():
|
|
13
|
+
"""
|
|
14
|
+
Initialize the 'config' database and create the `server_config` table if it doesn't exist.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
DatabaseManager.init_databases()
|
|
18
|
+
logging.info("✅ [APP] Configuration database initialized successfully.")
|
|
19
|
+
except Exception as e:
|
|
20
|
+
logging.exception("❌ [APP] Failed to initialize the configuration database.")
|
|
21
|
+
raise RuntimeError("Database initialization failed.") from e
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def is_config_initialized() -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Check if the configuration is already initialized.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
bool: True if configuration exists, False otherwise.
|
|
30
|
+
"""
|
|
31
|
+
session = None
|
|
32
|
+
try:
|
|
33
|
+
session = DatabaseManager.get_session("config")
|
|
34
|
+
config_count = session.query(ConfigEntity).count()
|
|
35
|
+
return config_count > 0 # True if at least one config exists
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logging.exception("❌ [APP] Failed to check if configuration is initialized.")
|
|
38
|
+
return False
|
|
39
|
+
finally:
|
|
40
|
+
if session:
|
|
41
|
+
session.close()
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def set_config(key: str, value: str):
|
|
45
|
+
"""
|
|
46
|
+
Set or update a configuration key-value pair in the 'config' database.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
key (str): The configuration key.
|
|
50
|
+
value (str): The configuration value.
|
|
51
|
+
"""
|
|
52
|
+
if not key or not isinstance(key, str):
|
|
53
|
+
raise ValueError("⚠️ [APP] The 'key' must be a non-empty string.")
|
|
54
|
+
if not isinstance(value, str):
|
|
55
|
+
raise ValueError("⚠️ [APP] The 'value' must be a string.")
|
|
56
|
+
|
|
57
|
+
session = None
|
|
58
|
+
try:
|
|
59
|
+
session = DatabaseManager.get_session("config")
|
|
60
|
+
logging.info(f"🔧 Attempting to set configuration: {key} = {value}")
|
|
61
|
+
existing_config = session.query(ConfigEntity).filter_by(key=key).first()
|
|
62
|
+
if existing_config:
|
|
63
|
+
logging.info(f"🔄 [APP] Updating configuration key: {key}")
|
|
64
|
+
existing_config.value = value
|
|
65
|
+
else:
|
|
66
|
+
logging.info(f"➕ [APP] Adding new configuration key: {key}")
|
|
67
|
+
new_config = ConfigEntity(key=key, value=value)
|
|
68
|
+
session.add(new_config)
|
|
69
|
+
session.commit()
|
|
70
|
+
logging.info(f"✅ [APP] Configuration key '{key}' set successfully.")
|
|
71
|
+
except Exception as e:
|
|
72
|
+
if session:
|
|
73
|
+
session.rollback()
|
|
74
|
+
logging.exception(f"❌ [APP] Failed to set configuration key '{key}': {e}")
|
|
75
|
+
raise RuntimeError(f"Failed to set configuration key '{key}'") from e
|
|
76
|
+
finally:
|
|
77
|
+
if session:
|
|
78
|
+
session.close()
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def set_config_batch(configs: dict):
|
|
82
|
+
"""
|
|
83
|
+
Set or update multiple configuration key-value pairs in the 'config' database in a batch operation.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
configs (dict): A dictionary containing configuration key-value pairs.
|
|
87
|
+
"""
|
|
88
|
+
if not isinstance(configs, dict) or not configs:
|
|
89
|
+
raise ValueError("⚠️ [APP] The 'configs' parameter must be a non-empty dictionary.")
|
|
90
|
+
|
|
91
|
+
session = None
|
|
92
|
+
try:
|
|
93
|
+
session = DatabaseManager.get_session("config")
|
|
94
|
+
logging.info(f"🔄 [APP] Attempting to set {len(configs)} configuration keys in batch.")
|
|
95
|
+
|
|
96
|
+
existing_configs = session.query(ConfigEntity).filter(ConfigEntity.key.in_(configs.keys())).all()
|
|
97
|
+
existing_keys = {config.key: config for config in existing_configs}
|
|
98
|
+
|
|
99
|
+
for key, value in configs.items():
|
|
100
|
+
if key in existing_keys:
|
|
101
|
+
logging.info(f"🔄 [APP] Updating configuration key: {key}")
|
|
102
|
+
existing_keys[key].value = value
|
|
103
|
+
else:
|
|
104
|
+
logging.info(f"➕ [APP] Adding new configuration key: {key}")
|
|
105
|
+
new_config = ConfigEntity(key=key, value=value)
|
|
106
|
+
session.add(new_config)
|
|
107
|
+
|
|
108
|
+
session.commit()
|
|
109
|
+
logging.info("✅ [APP] All configuration keys set successfully.")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
if session:
|
|
112
|
+
session.rollback()
|
|
113
|
+
logging.exception(f"❌ [APP] Failed to set batch configuration keys: {e}")
|
|
114
|
+
raise RuntimeError("Failed to set batch configuration keys.") from e
|
|
115
|
+
finally:
|
|
116
|
+
if session:
|
|
117
|
+
session.close()
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def get_config(key: str) -> str:
|
|
121
|
+
"""
|
|
122
|
+
Retrieve the value of a specific configuration key from the 'config' database.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
key (str): The configuration key.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
str: The configuration value, or None if the key does not exist.
|
|
129
|
+
"""
|
|
130
|
+
if not key or not isinstance(key, str):
|
|
131
|
+
raise ValueError("⚠️ The 'key' must be a non-empty string.")
|
|
132
|
+
|
|
133
|
+
session = None
|
|
134
|
+
try:
|
|
135
|
+
session = DatabaseManager.get_session("config")
|
|
136
|
+
logging.info(f"🔍 [APP] Retrieving configuration key: {key}")
|
|
137
|
+
config = session.query(ConfigEntity).filter_by(key=key).first()
|
|
138
|
+
if config:
|
|
139
|
+
logging.info(f"✅ [APP] Configuration key '{key}' retrieved successfully.")
|
|
140
|
+
return config.value
|
|
141
|
+
else:
|
|
142
|
+
logging.warning(f"⚠️ [APP] Configuration key '{key}' not found.")
|
|
143
|
+
return None
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logging.exception(f"❌ [APP] Failed to retrieve configuration key '{key}': {e}")
|
|
146
|
+
raise RuntimeError(f"Failed to retrieve configuration key '{key}'") from e
|
|
147
|
+
finally:
|
|
148
|
+
if session:
|
|
149
|
+
session.close()
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def get_all_configs() -> dict:
|
|
153
|
+
"""
|
|
154
|
+
Retrieve all configuration key-value pairs from the 'config' database.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
dict: A dictionary of all configuration key-value pairs.
|
|
158
|
+
"""
|
|
159
|
+
session = None
|
|
160
|
+
try:
|
|
161
|
+
session = DatabaseManager.get_session("config")
|
|
162
|
+
logging.info("🔍 [APP] Retrieving all configuration keys.")
|
|
163
|
+
configs = session.query(ConfigEntity).all()
|
|
164
|
+
if configs:
|
|
165
|
+
logging.info("✅ [APP] All configuration keys retrieved successfully.")
|
|
166
|
+
return {config.key: config.value for config in configs}
|
|
167
|
+
else:
|
|
168
|
+
logging.info("⚠️ [APP] No configuration keys found.")
|
|
169
|
+
return {}
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logging.exception("❌ [APP] Failed to retrieve all configuration keys.")
|
|
172
|
+
raise RuntimeError("Failed to retrieve all configuration keys.") from e
|
|
173
|
+
finally:
|
|
174
|
+
if session:
|
|
175
|
+
session.close()
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def print_config():
|
|
179
|
+
"""
|
|
180
|
+
Print all configuration key-value pairs to the console.
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
configs = ConfigurationManager.get_all_configs()
|
|
184
|
+
if configs:
|
|
185
|
+
print("📄 Current Configuration:")
|
|
186
|
+
for key, value in configs.items():
|
|
187
|
+
# Mask sensitive information completely
|
|
188
|
+
if key.lower() in ['token', 'password']:
|
|
189
|
+
print(f" 🔹 {key}: ***")
|
|
190
|
+
else:
|
|
191
|
+
print(f" 🔹 {key}: {value}")
|
|
192
|
+
else:
|
|
193
|
+
print("⚠️ No configuration found. Please initialize the configuration.")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logging.exception("❌ Failed to print configuration keys.")
|
|
196
|
+
raise RuntimeError("Failed to print configuration keys.") from e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from alembic.migration import MigrationContext
|
|
3
|
+
from alembic.operations import Operations
|
|
4
|
+
from alembic.operations.ops import AlterColumnOp
|
|
5
|
+
from alembic.autogenerate import produce_migrations
|
|
6
|
+
from sqlalchemy import Column, Connection, Table, create_engine, inspect, MetaData, select, text
|
|
7
|
+
from sqlalchemy.orm import sessionmaker, declarative_base, scoped_session
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
# Base class for ORM models
|
|
12
|
+
Base = declarative_base()
|
|
13
|
+
|
|
14
|
+
# Dictionary to hold engines and session factories
|
|
15
|
+
ENGINES = {}
|
|
16
|
+
SESSION_FACTORIES = {}
|
|
17
|
+
_initialized = False # To track whether databases have been initialized
|
|
18
|
+
_storage_path = None # Global storage path
|
|
19
|
+
|
|
20
|
+
def set_storage_path(storage_path: str):
|
|
21
|
+
"""Set the global storage path for the application."""
|
|
22
|
+
global _storage_path
|
|
23
|
+
_storage_path = Path(storage_path)
|
|
24
|
+
|
|
25
|
+
def _get_storage_paths():
|
|
26
|
+
"""Get storage paths using the configured storage path."""
|
|
27
|
+
global _storage_path
|
|
28
|
+
if _storage_path is None:
|
|
29
|
+
_storage_path = Path("data") # Default fallback
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
"db": _storage_path / "sqlite",
|
|
33
|
+
"files": _storage_path / "files",
|
|
34
|
+
"models": _storage_path / "model",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def get_storage_path(subdir: str = None) -> Path:
|
|
38
|
+
"""Get a storage path for a specific subdirectory."""
|
|
39
|
+
global _storage_path
|
|
40
|
+
if _storage_path is None:
|
|
41
|
+
_storage_path = Path("data") # Default fallback
|
|
42
|
+
|
|
43
|
+
if subdir:
|
|
44
|
+
return _storage_path / subdir
|
|
45
|
+
return _storage_path
|
|
46
|
+
|
|
47
|
+
def alter_drop_not_null_sqlite(conn: Connection, table_name: str, column_name: str):
|
|
48
|
+
# Load existing metadata
|
|
49
|
+
metadata = MetaData()
|
|
50
|
+
metadata.reflect(conn)
|
|
51
|
+
|
|
52
|
+
table = metadata.tables[table_name]
|
|
53
|
+
|
|
54
|
+
# Create new table with updated schema (column without NOT NULL)
|
|
55
|
+
new_columns = []
|
|
56
|
+
for col in table.columns:
|
|
57
|
+
new_col = Column(
|
|
58
|
+
col.name,
|
|
59
|
+
col.type,
|
|
60
|
+
nullable=(True if col.name == column_name else col.nullable),
|
|
61
|
+
primary_key=col.primary_key,
|
|
62
|
+
default=col.default,
|
|
63
|
+
server_default=col.server_default
|
|
64
|
+
)
|
|
65
|
+
new_columns.append(new_col)
|
|
66
|
+
|
|
67
|
+
new_table = Table(f"{table_name}_new", MetaData(), *new_columns)
|
|
68
|
+
new_table.create(conn)
|
|
69
|
+
|
|
70
|
+
# Copy data
|
|
71
|
+
data = conn.execute(select(table)).mappings().all()
|
|
72
|
+
if data:
|
|
73
|
+
conn.execute(new_table.insert(), data)
|
|
74
|
+
|
|
75
|
+
# Drop old table, rename new table
|
|
76
|
+
table.drop(conn)
|
|
77
|
+
conn.execute(text(f"ALTER TABLE {table_name}_new RENAME TO {table_name}"))
|
|
78
|
+
|
|
79
|
+
class DatabaseManager:
|
|
80
|
+
"""
|
|
81
|
+
Manages multiple database connections, engines, and session factories.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def init_databases():
|
|
86
|
+
"""
|
|
87
|
+
Initialize databases and create tables for all models based on their `__bind_key__`.
|
|
88
|
+
"""
|
|
89
|
+
global _initialized
|
|
90
|
+
if _initialized:
|
|
91
|
+
logging.info("✅ Databases already initialized.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Get storage paths with lazy loading
|
|
95
|
+
storage_paths = _get_storage_paths()
|
|
96
|
+
|
|
97
|
+
# Define database paths
|
|
98
|
+
DB_PATHS = {
|
|
99
|
+
"default": storage_paths["db"] / "default.db",
|
|
100
|
+
"config": storage_paths["db"] / "config.db",
|
|
101
|
+
"logging": storage_paths["db"] / "logging.db",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Initialize engines and session factories for each database
|
|
105
|
+
for name, path in DB_PATHS.items():
|
|
106
|
+
path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists
|
|
107
|
+
engine = create_engine(f"sqlite:///{path.as_posix()}")
|
|
108
|
+
ENGINES[name] = engine
|
|
109
|
+
SESSION_FACTORIES[name] = scoped_session(sessionmaker(bind=engine)) # Use scoped sessions
|
|
110
|
+
DatabaseManager.synchronize(name)
|
|
111
|
+
|
|
112
|
+
_initialized = True
|
|
113
|
+
logging.info("✅ [APP] Databases initialized successfully.")
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def synchronize(db_name):
|
|
117
|
+
engine = ENGINES[db_name]
|
|
118
|
+
|
|
119
|
+
models = []
|
|
120
|
+
for mapper_object in Base.registry.mappers:
|
|
121
|
+
model_class = mapper_object.class_
|
|
122
|
+
if hasattr(model_class, "__bind_key__") and model_class.__bind_key__ == db_name:
|
|
123
|
+
models.append(model_class)
|
|
124
|
+
|
|
125
|
+
inspector = inspect(engine)
|
|
126
|
+
dialect = inspector.dialect.name
|
|
127
|
+
existing_tables = inspector.get_table_names()
|
|
128
|
+
|
|
129
|
+
if not existing_tables:
|
|
130
|
+
if not models:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
for model in models:
|
|
134
|
+
model.metadata.create_all(bind=engine)
|
|
135
|
+
|
|
136
|
+
logging.info(f"✅ [APP] Created initial tables for '{db_name}' database")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
connection: Connection = engine.connect()
|
|
140
|
+
try:
|
|
141
|
+
with connection.begin():
|
|
142
|
+
context = MigrationContext.configure(connection)
|
|
143
|
+
|
|
144
|
+
target_metadata = MetaData()
|
|
145
|
+
for model in models:
|
|
146
|
+
for table in model.metadata.tables.values():
|
|
147
|
+
if table.name not in target_metadata.tables:
|
|
148
|
+
table.tometadata(target_metadata)
|
|
149
|
+
|
|
150
|
+
ops_list = produce_migrations(context, target_metadata).upgrade_ops_list
|
|
151
|
+
|
|
152
|
+
if ops_list[0].ops:
|
|
153
|
+
logging.info(f"⚡ [APP] Detected schema changes in '{db_name}' database. Applying migrations...")
|
|
154
|
+
|
|
155
|
+
op = Operations(context)
|
|
156
|
+
for upgrade_ops in ops_list:
|
|
157
|
+
for schema_element in upgrade_ops.ops:
|
|
158
|
+
if hasattr(schema_element, 'ops'):
|
|
159
|
+
for table_op in schema_element.ops:
|
|
160
|
+
if dialect == 'sqlite' and isinstance(table_op, AlterColumnOp):
|
|
161
|
+
alter_drop_not_null_sqlite(connection, table_op.table_name, table_op.column_name)
|
|
162
|
+
else:
|
|
163
|
+
op.invoke(table_op)
|
|
164
|
+
else:
|
|
165
|
+
if dialect == 'sqlite' and isinstance(schema_element, AlterColumnOp):
|
|
166
|
+
alter_drop_not_null_sqlite(connection, schema_element.table_name, schema_element.column_name)
|
|
167
|
+
else:
|
|
168
|
+
op.invoke(schema_element)
|
|
169
|
+
|
|
170
|
+
for diff in upgrade_ops.as_diffs():
|
|
171
|
+
if diff[0] == 'add_table':
|
|
172
|
+
table = diff[1]
|
|
173
|
+
logging.info(f"🆕 [APP] Creating new table: {table.name}")
|
|
174
|
+
|
|
175
|
+
elif diff[0] == 'remove_table':
|
|
176
|
+
table_name = diff[1].name
|
|
177
|
+
logging.info(f"🗑️ [APP] Removing table: {table_name}")
|
|
178
|
+
|
|
179
|
+
elif diff[0] == 'add_column':
|
|
180
|
+
table_name, column = diff[2], diff[3]
|
|
181
|
+
logging.info(f"➕ [APP] Adding column: {column.name} to table {table_name}")
|
|
182
|
+
|
|
183
|
+
elif diff[0] == 'remove_column':
|
|
184
|
+
table_name, column = diff[2], diff[3]
|
|
185
|
+
logging.info(f"➖ [APP] Removing column: {column.name} from table {table_name}")
|
|
186
|
+
|
|
187
|
+
logging.info(f"✅ [APP] Schema migrations for '{db_name}' database completed successfully")
|
|
188
|
+
else:
|
|
189
|
+
logging.info(f"✅ [APP] No schema changes detected for '{db_name}' database")
|
|
190
|
+
|
|
191
|
+
finally:
|
|
192
|
+
connection.close()
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def get_session(db_name: str = "default"):
|
|
196
|
+
"""
|
|
197
|
+
Provide a session object for the specified database.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
db_name (str): The name of the database connection.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
sqlalchemy.orm.Session: A session for the specified database.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ValueError: If the database name is invalid.
|
|
207
|
+
"""
|
|
208
|
+
if db_name not in SESSION_FACTORIES:
|
|
209
|
+
raise ValueError(f"❌ Invalid database name: {db_name}")
|
|
210
|
+
return SESSION_FACTORIES[db_name]
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def shutdown():
|
|
214
|
+
"""
|
|
215
|
+
Cleanly dispose of all database connections.
|
|
216
|
+
"""
|
|
217
|
+
for name, engine in ENGINES.items():
|
|
218
|
+
engine.dispose()
|
|
219
|
+
logging.info("🛑 All database connections have been closed.")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|