reactor-runtime 0.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.
@@ -0,0 +1,14 @@
1
+ """Reactor CLI Commands
2
+
3
+ This module contains all command implementations for the reactor CLI.
4
+ Each command is implemented as a class following the HuggingFace pattern.
5
+ """
6
+
7
+ from .run import RunCommand
8
+ from .init import InitCommand
9
+ from .download import DownloadCommand
10
+ from .upload import UploadCommand
11
+ from .setup import SetupCommand
12
+ from .capabilities import CapabilitiesCommand
13
+
14
+ __all__ = ['RunCommand', 'InitCommand', 'DownloadCommand', 'UploadCommand', 'SetupCommand', 'CapabilitiesCommand']
@@ -0,0 +1,118 @@
1
+ """Run command implementation."""
2
+
3
+ import importlib
4
+ import json
5
+ from reactor_runtime.model_api import VideoModel, command
6
+ import ast
7
+ from pathlib import Path
8
+
9
+ def load_class_without_init(file_path: str, class_name: str):
10
+ """
11
+ Load only the subclass definition from a Python file (without executing imports or __init__),
12
+ recreate it with CommandBase as its parent, and call its inherited commands() method.
13
+ All imports from the source file are detected via AST and replicated in the namespace.
14
+ """
15
+ source = Path(file_path).read_text(encoding="utf-8")
16
+ tree = ast.parse(source, filename=file_path)
17
+
18
+ def extract_imports(tree):
19
+ """Return a dict of {imported_name: module_object or attribute} from the AST."""
20
+ imports = {}
21
+
22
+ for node in tree.body:
23
+ if isinstance(node, ast.Import):
24
+ for alias in node.names:
25
+ mod_name = alias.name
26
+ as_name = alias.asname or mod_name.split(".")[0]
27
+ try:
28
+ imports[as_name] = importlib.import_module(mod_name)
29
+ except ImportError:
30
+ pass # Skip missing modules safely
31
+
32
+ elif isinstance(node, ast.ImportFrom):
33
+ if node.module is None:
34
+ continue
35
+ try:
36
+ mod = importlib.import_module(node.module)
37
+ except ImportError:
38
+ continue
39
+ for alias in node.names:
40
+ as_name = alias.asname or alias.name
41
+ if alias.name == "*":
42
+ # Handle "from X import *" by copying all public symbols
43
+ for name in dir(mod):
44
+ if not name.startswith("_"):
45
+ imports[name] = getattr(mod, name)
46
+ else:
47
+ try:
48
+ imports[as_name] = getattr(mod, alias.name)
49
+ except AttributeError:
50
+ pass
51
+
52
+ return imports
53
+
54
+ # Detect and import all modules referenced in the source
55
+ imported_symbols = extract_imports(tree)
56
+
57
+ for node in tree.body:
58
+ if isinstance(node, ast.ClassDef) and node.name == class_name:
59
+ # Replace base classes with CommandBase
60
+ node.bases = [ast.Name(id='CommandBase', ctx=ast.Load())]
61
+ ast.fix_missing_locations(node)
62
+
63
+ # Compile a module containing only that class
64
+ class_module = ast.Module(body=[node], type_ignores=[])
65
+ ast.fix_missing_locations(class_module)
66
+ code = compile(class_module, filename=file_path, mode="exec")
67
+
68
+ # Build isolated namespace
69
+ ns = {
70
+ "CommandBase": VideoModel,
71
+ "command": command,
72
+ **imported_symbols
73
+ }
74
+
75
+ exec(code, ns)
76
+ subcls = ns[class_name]
77
+ obj = subcls.__new__(subcls)
78
+ return obj
79
+
80
+ raise ValueError(f"Class '{class_name}' not found in {file_path}")
81
+
82
+
83
+ class CapabilitiesCommand:
84
+ @staticmethod
85
+ def register_subcommand(subparsers):
86
+ """Register capabilities command"""
87
+ run_parser = subparsers.add_parser("capabilities", help="Print the capabilities of a reactor VideoModel.")
88
+ run_parser.set_defaults(func=CapabilitiesCommand)
89
+
90
+ def __init__(self, args):
91
+ """Initialize with parsed arguments"""
92
+ self.args = args
93
+
94
+ def run(self):
95
+ """Print the capabilities of a reactor VideoModel."""
96
+ from ..main import verify_reactor_workspace
97
+
98
+ # Verify workspace and get manifest data
99
+ manifest_data = verify_reactor_workspace()
100
+ if manifest_data is None:
101
+ return
102
+
103
+ # Extract model information from manifest
104
+ model_class_name: str = manifest_data["class"]
105
+ model_file, model_class = model_class_name.split(":")
106
+ model_class: VideoModel = load_class_without_init(model_file+".py", model_class)
107
+
108
+ if "model_name" not in manifest_data.keys():
109
+ print("Error: manifest.json is missing required 'model_name' field.")
110
+ print("Please add a 'model_name' field specifying the model name.")
111
+ return
112
+ if "model_version" not in manifest_data.keys():
113
+ print("Error: manifest.json is missing required 'model_version' field.")
114
+ print("Please add a 'model_version' field specifying the model version.")
115
+ return
116
+
117
+
118
+ print(json.dumps(model_class.commands(), indent=4))
@@ -0,0 +1,217 @@
1
+ """Download command implementation."""
2
+
3
+ import os
4
+ import json
5
+ import logging
6
+ from typing import List, Optional
7
+ from pathlib import Path
8
+ from supabase import create_client
9
+ from ..utils import get_weights_parallel, get_latest_version
10
+ from ..main import verify_reactor_workspace
11
+
12
+ # Set up logger
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def parse_manifest(manifest_str: str) -> dict:
17
+ """Parse a manifest string into a dictionary.
18
+
19
+ Args:
20
+ manifest_str: JSON string containing the manifest
21
+
22
+ Returns:
23
+ Parsed manifest dictionary
24
+ """
25
+ try:
26
+ return json.loads(manifest_str)
27
+ except json.JSONDecodeError as e:
28
+ logger.error(f"Failed to parse manifest JSON: {e}")
29
+ raise ValueError(f"Invalid manifest JSON: {e}")
30
+
31
+
32
+ def get_weights_from_manifest(manifest: dict) -> List[str]:
33
+ """Extract the weights list from a manifest dictionary.
34
+
35
+ Args:
36
+ manifest: Dictionary containing manifest data
37
+
38
+ Returns:
39
+ List of weight folder names
40
+ """
41
+ if "weights" not in manifest:
42
+ logger.warning("Manifest does not contain a 'weights' field")
43
+ return []
44
+
45
+ weights = manifest["weights"]
46
+ if not isinstance(weights, list):
47
+ logger.error(f"'weights' field must be a list, got {type(weights)}")
48
+ raise ValueError("'weights' field in manifest must be a list")
49
+
50
+ return weights
51
+
52
+
53
+ def get_weights_from_local_manifest() -> List[str]:
54
+ """Read weights from local manifest.json file.
55
+
56
+ Returns:
57
+ List of weight folder names from local manifest
58
+ """
59
+ manifest = verify_reactor_workspace()
60
+ if not manifest:
61
+ raise RuntimeError("Failed to verify reactor workspace or read manifest.json")
62
+
63
+ return get_weights_from_manifest(manifest)
64
+
65
+
66
+ def get_weights_from_model_id(model_id: str) -> List[str]:
67
+ """Fetch model manifest from Supabase and extract weights.
68
+
69
+ Args:
70
+ model_id: Model identifier with optional version (e.g., "matrix-2" or "matrix-2@v1.0")
71
+ If no version is specified, defaults to "latest"
72
+
73
+ Returns:
74
+ List of weight folder names from the model's manifest
75
+ """
76
+ # Parse model_id and version from format: model@version or just model
77
+ if '@' in model_id:
78
+ model_name, version = model_id.split('@', 1)
79
+ else:
80
+ model_name = model_id
81
+ version = "latest"
82
+
83
+ supabase = create_client(
84
+ os.getenv('SUPABASE_URL'),
85
+ os.getenv('SUPABASE_SERVICE_KEY')
86
+ )
87
+
88
+ # If version is "latest", we need to find the actual latest version
89
+ if version == "latest":
90
+ logger.info(f"Resolving latest version for model '{model_name}'...")
91
+
92
+ # Get the latest version using our utility function
93
+ try:
94
+ version = get_latest_version(model_name)
95
+ logger.info(f"Resolved latest version for '{model_name}': {version}")
96
+ except (ValueError, RuntimeError) as e:
97
+ raise ValueError(f"Failed to determine latest version for model '{model_name}': {e}")
98
+
99
+ logger.info(f"Fetching manifest for model '{model_name}' version '{version}'...")
100
+
101
+ result = supabase.table('models').select('manifest').eq(
102
+ 'model_id', model_name
103
+ ).eq('version', version).execute()
104
+
105
+ if not result.data:
106
+ raise ValueError(f"Model '{model_name}' version '{version}' not found in database")
107
+
108
+ manifest = result.data[0]['manifest']
109
+
110
+ if not manifest:
111
+ raise ValueError(f"Model '{model_name}' version '{version}' doesn't have a manifest in database.")
112
+
113
+ return get_weights_from_manifest(manifest)
114
+
115
+
116
+ def get_weights_from_model_ids(model_ids: List[str]) -> List[str]:
117
+ """Fetch manifests for multiple models and extract unique weights.
118
+
119
+ Args:
120
+ model_ids: List of model identifiers with optional versions
121
+ (e.g., ["matrix-2", "longlive@v1.0", "mk64"])
122
+
123
+ Returns:
124
+ Deduplicated list of weight folder names from all models
125
+ """
126
+ all_weights = set()
127
+
128
+ for model_id in model_ids:
129
+ try:
130
+ weights = get_weights_from_model_id(model_id)
131
+ all_weights.update(weights)
132
+ logger.info(f"Model '{model_id}' requires weights: {weights}")
133
+ except Exception as e:
134
+ logger.error(f"Failed to fetch weights for model '{model_id}': {e}")
135
+ raise
136
+
137
+ unique_weights = list(all_weights)
138
+ logger.info(f"Total unique weights across all models: {unique_weights}")
139
+ return unique_weights
140
+
141
+
142
+ class DownloadCommand:
143
+ @staticmethod
144
+ def register_subcommand(subparsers):
145
+ """Register download command"""
146
+ download_parser = subparsers.add_parser(
147
+ "download",
148
+ help="Download model weights"
149
+ )
150
+ download_parser.add_argument(
151
+ "--weights",
152
+ nargs='+',
153
+ help="List of weight folder names to download"
154
+ )
155
+ download_parser.add_argument(
156
+ "--models",
157
+ nargs='+',
158
+ help="List of model identifiers to fetch weights from (e.g., matrix-2 longlive@v1.0). Use model@version format to specify version, defaults to 'latest'"
159
+ )
160
+ download_parser.add_argument(
161
+ "--no-cache",
162
+ action="store_true",
163
+ help="Force re-download even if weights exist locally"
164
+ )
165
+ download_parser.set_defaults(func=DownloadCommand)
166
+
167
+ def __init__(self, args):
168
+ """Initialize command with parsed arguments"""
169
+ self.args = args
170
+
171
+ def run(self):
172
+ """Download model weights from registry"""
173
+ try:
174
+ # Determine which route to take based on arguments
175
+ weights_list: Optional[List[str]] = None
176
+
177
+ if self.args.weights:
178
+ # Route 1: Explicit weights list provided
179
+ weights_list = self.args.weights
180
+ logger.info(f"Downloading specified weights: {weights_list}")
181
+
182
+ elif self.args.models:
183
+ # Route 2: Fetch from model IDs in Supabase and deduplicate
184
+ print(f"Fetching manifests for {len(self.args.models)} model(s)...")
185
+ weights_list = get_weights_from_model_ids(self.args.models)
186
+ print(f"Found {len(weights_list)} unique weight(s) across all models")
187
+ logger.info(f"Downloaded manifests for models {self.args.models}, unique weights: {weights_list}")
188
+
189
+ else:
190
+ # Route 3: Default - read from local manifest.json
191
+ logger.info("No arguments specified, reading from local manifest.json...")
192
+ weights_list = get_weights_from_local_manifest()
193
+ logger.info(f"Found weights in local manifest: {weights_list}")
194
+
195
+ if not weights_list:
196
+ print("No weights to download.")
197
+ return
198
+
199
+ # Download weights in parallel
200
+ if self.args.no_cache:
201
+ print(f"Force downloading {len(weights_list)} weight folder(s) (no-cache mode)...")
202
+ else:
203
+ print(f"Downloading {len(weights_list)} weight folder(s)...")
204
+ weight_paths = get_weights_parallel(weights_list, no_cache=self.args.no_cache)
205
+
206
+ # Print results
207
+ print("\nDownload complete! Weight paths:")
208
+ for i, (weight_name, weight_path) in enumerate(zip(weights_list, weight_paths), 1):
209
+ if weight_path:
210
+ print(f" {i}. {weight_name}: {weight_path}")
211
+ else:
212
+ print(f" {i}. {weight_name}: FAILED")
213
+
214
+ except Exception as e:
215
+ logger.error(f"Download command failed: {e}")
216
+ print(f"Error: {e}")
217
+ return
@@ -0,0 +1,86 @@
1
+ """Init command implementation."""
2
+
3
+ import pathlib
4
+ import importlib.resources
5
+
6
+
7
+ class InitCommand:
8
+ @staticmethod
9
+ def register_subcommand(subparsers):
10
+ """Register init command"""
11
+ init_parser = subparsers.add_parser("init", help="Initialize a new reactor model workspace")
12
+ init_parser.add_argument("name", help="Name of the model (will create directory with this name)")
13
+ init_parser.set_defaults(func=InitCommand)
14
+
15
+ def __init__(self, args):
16
+ """Initialize with parsed arguments"""
17
+ self.args = args
18
+
19
+ def run(self):
20
+ """Initialize a new reactor model workspace in a directory with the given name."""
21
+ model_name = self.args.name
22
+ current_dir = pathlib.Path.cwd()
23
+ target_dir = current_dir / model_name
24
+
25
+ # Check if directory already exists
26
+ if target_dir.exists():
27
+ print(f"Error: Directory '{model_name}' already exists")
28
+ print(f"Please choose a different name or remove the existing directory")
29
+ return
30
+
31
+ # Create the model directory
32
+ try:
33
+ target_dir.mkdir()
34
+ print(f"Created directory: {model_name}")
35
+ except Exception as e:
36
+ print(f"Error creating directory '{model_name}': {e}")
37
+ return
38
+
39
+ print(f"Initializing reactor workspace in {target_dir}")
40
+
41
+ # Copy template files from the installed package
42
+ try:
43
+ import template as template_package
44
+ template_files = [
45
+ "model_template.py",
46
+ "manifest.json",
47
+ "requirements.txt",
48
+ "Dockerfile",
49
+ "README.md"
50
+ ]
51
+
52
+ for filename in template_files:
53
+ dest_path = target_dir / filename
54
+ if dest_path.exists():
55
+ print(f"Warning: {filename} already exists, skipping...")
56
+ continue
57
+
58
+ # Read template file from package resources
59
+ try:
60
+ with importlib.resources.open_text(template_package, filename) as f:
61
+ content = f.read()
62
+
63
+ # Write to destination
64
+ with open(dest_path, 'w') as f:
65
+ f.write(content)
66
+
67
+ print(f"Created: {filename}")
68
+ except FileNotFoundError:
69
+ print(f"Warning: Template file {filename} not found in package")
70
+ continue
71
+
72
+ print(f"\nReactor workspace '{model_name}' initialized successfully!")
73
+ print(f"\nNext steps:")
74
+ print(f"1. cd {model_name}")
75
+ print("2. Edit model_template.py to implement your video model")
76
+ print("3. Update manifest.json with your model class, parameters, and reactor-runtime version")
77
+ print("4. Add any additional dependencies to requirements.txt")
78
+ print("5. Run 'reactor run' to start your model")
79
+ print(f"6. Build Docker image: docker build --build-arg GITHUB_TOKEN=<github_token> -t {model_name} .")
80
+
81
+ except ImportError:
82
+ print("Error: reactor_runtime package not found. Make sure it's properly installed.")
83
+ return
84
+ except Exception as e:
85
+ print(f"Error initializing workspace: {e}")
86
+ return
@@ -0,0 +1,118 @@
1
+ """Run command implementation."""
2
+
3
+ import json
4
+ import os
5
+ from reactor_runtime.utils.launch import run_reactor_runtime_sync
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class RunCommand:
11
+ @staticmethod
12
+ def register_subcommand(subparsers):
13
+ """Register run command"""
14
+ run_parser = subparsers.add_parser("run", help="Run reactor runtime with model from manifest.json")
15
+ run_parser.add_argument(
16
+ "--deploy",
17
+ action="store_true",
18
+ help="Run the model in a proper deployment mode."
19
+ )
20
+ run_parser.add_argument(
21
+ "--debug","--headless",
22
+ action="store_true",
23
+ help="Run the model in a headless, debug mode."
24
+ )
25
+ run_parser.add_argument(
26
+ "--host",
27
+ type=str,
28
+ default="0.0.0.0",
29
+ help="Host to bind the FastAPI Server. Default: 0.0.0.0"
30
+ )
31
+ run_parser.add_argument(
32
+ "--port",
33
+ type=int,
34
+ default=8081,
35
+ help="FastAPI Server port. Default: 8081"
36
+ )
37
+ run_parser.add_argument(
38
+ "--log-level",
39
+ type=str,
40
+ default="INFO",
41
+ choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
42
+ help="Logging level. Default: INFO"
43
+ )
44
+ run_parser.set_defaults(func=RunCommand)
45
+
46
+ def __init__(self, args):
47
+ """Initialize with parsed arguments"""
48
+ self.args = args
49
+
50
+ def run(self):
51
+ """Run the reactor runtime with the model specified in manifest.json."""
52
+ from ..main import verify_reactor_workspace
53
+
54
+ # Verify workspace and get manifest data
55
+ manifest_data = verify_reactor_workspace()
56
+ if manifest_data is None:
57
+ return
58
+
59
+ # Extract model information from manifest
60
+ model_class = manifest_data["class"]
61
+ model_args = manifest_data.get("args", {})
62
+
63
+ if "model_name" not in manifest_data.keys():
64
+ print("Error: manifest.json is missing required 'model_name' field.")
65
+ print("Please add a 'model_name' field specifying the model name.")
66
+ return
67
+ if "model_version" not in manifest_data.keys():
68
+ print("Error: manifest.json is missing required 'model_version' field.")
69
+ print("Please add a 'model_version' field specifying the model version.")
70
+ return
71
+
72
+ print(f"Starting reactor runtime...")
73
+ print(f"Model: {model_class}")
74
+ if model_args:
75
+ print(f"Model args: {model_args}")
76
+
77
+ runtime_serve_fn = None
78
+ if self.args.deploy:
79
+ try:
80
+ from reactor_runtime.context._cloud.cloud_runtime import serve
81
+ if os.getenv("REDIS_URL", None) is None:
82
+ raise KeyError("REDIS_URL environment variable is not set. Please set it to the Redis URL.")
83
+ runtime_serve_fn = serve
84
+ except KeyError as e:
85
+ logger.error(f"Error starting reactor runtime: {e}")
86
+ return
87
+ except ModuleNotFoundError as e:
88
+ logger.error("Deploy mode is not supported locally. Please use the command without --deploy.")
89
+ return
90
+ else:
91
+ if self.args.debug:
92
+ try:
93
+ from reactor_runtime.context.debug.debug_runtime import serve
94
+ runtime_serve_fn = serve
95
+ except ModuleNotFoundError:
96
+ logger.error("Debug mode is not available. Please use the command without --debug/--headless.")
97
+ return
98
+ else:
99
+ from reactor_runtime.context.local.local_runtime import serve
100
+ runtime_serve_fn = serve
101
+
102
+ try:
103
+ run_reactor_runtime_sync(
104
+ runtime_serve_fn=runtime_serve_fn,
105
+ model=model_class,
106
+ model_args=json.dumps(model_args) if model_args else None,
107
+ host=self.args.host,
108
+ port=self.args.port,
109
+ log_level=self.args.log_level,
110
+ model_name=manifest_data["model_name"],
111
+ model_version=manifest_data["model_version"],
112
+ )
113
+
114
+ except KeyboardInterrupt:
115
+ print("\nReactor runtime stopped by user.")
116
+ except Exception as e:
117
+ print(f"Error running reactor runtime: {e}")
118
+ raise
@@ -0,0 +1,103 @@
1
+ """Setup command implementation.
2
+
3
+ Interactive setup for configuring Reactor weights management credentials.
4
+ This is for manual setup only - helps users configure their environment
5
+ variables for Supabase and AWS S3 access.
6
+ """
7
+
8
+ import os
9
+
10
+ class SetupCommand:
11
+ @staticmethod
12
+ def register_subcommand(subparsers):
13
+ """Register setup command"""
14
+ setup_parser = subparsers.add_parser("setup", help="Check reactor weights configuration")
15
+ setup_parser.set_defaults(func=SetupCommand)
16
+
17
+ def __init__(self, args):
18
+ """Initialize command with parsed arguments"""
19
+ self.args = args
20
+
21
+ def run(self):
22
+ """Interactive setup for reactor weights"""
23
+ print("Reactor weights setup")
24
+ print("This will help you configure Supabase and AWS credentials")
25
+
26
+ # Check current status
27
+ supabase_vars = {
28
+ "SUPABASE_URL": os.getenv("SUPABASE_URL"),
29
+ "SUPABASE_SERVICE_KEY": os.getenv("SUPABASE_SERVICE_KEY"),
30
+ }
31
+
32
+ aws_vars = {
33
+ "AWS_ACCESS_KEY_ID": os.getenv("AWS_ACCESS_KEY_ID"),
34
+ "AWS_SECRET_ACCESS_KEY": os.getenv("AWS_SECRET_ACCESS_KEY"),
35
+ }
36
+
37
+ # Setup Supabase
38
+ print("\nSupabase setup:")
39
+ print("Go to https://supabase.com/dashboard > Settings > API")
40
+
41
+ supabase_url = input(f"Supabase URL (current: {supabase_vars['SUPABASE_URL'] or 'not set'}): ").strip()
42
+ supabase_service_key = input(f"Supabase service key (current: {'***' if supabase_vars['SUPABASE_SERVICE_KEY'] else 'not set'}): ").strip()
43
+
44
+ # Setup AWS
45
+ print("\nAWS setup:")
46
+ print("Go to AWS Console > IAM > Users (create user with S3 permissions)")
47
+
48
+ aws_access_key = input(f"AWS Access Key ID (current: {'***' if aws_vars['AWS_ACCESS_KEY_ID'] else 'not set'}): ").strip()
49
+ aws_secret_key = input(f"AWS Secret Key (current: {'***' if aws_vars['AWS_SECRET_ACCESS_KEY'] else 'not set'}): ").strip()
50
+
51
+ # Generate export commands
52
+ print("\nAdd these to your shell profile (.bashrc, .zshrc, etc.):")
53
+
54
+ if supabase_url:
55
+ print(f"export SUPABASE_URL='{supabase_url}'")
56
+ if supabase_service_key:
57
+ print(f"export SUPABASE_SERVICE_KEY='{supabase_service_key}'")
58
+ if aws_access_key:
59
+ print(f"export AWS_ACCESS_KEY_ID='{aws_access_key}'")
60
+ if aws_secret_key:
61
+ print(f"export AWS_SECRET_ACCESS_KEY='{aws_secret_key}'")
62
+
63
+ print("\nRun 'source ~/.bashrc' (or restart terminal) to apply changes")
64
+
65
+ # Test connections if user entered values
66
+ if any([supabase_url, supabase_service_key, aws_access_key, aws_secret_key]):
67
+ print("\nTesting connections with provided credentials...")
68
+ self._test_connections(supabase_url, supabase_service_key, aws_access_key, aws_secret_key)
69
+
70
+ def _test_connections(self, supabase_url=None, supabase_service_key=None, aws_access_key=None, aws_secret_key=None):
71
+ """Test connections with provided credentials"""
72
+
73
+ # Test Supabase with provided values or environment
74
+ try:
75
+ from supabase import create_client
76
+ url = supabase_url or os.getenv('SUPABASE_URL')
77
+ key = supabase_service_key or os.getenv('SUPABASE_SERVICE_KEY')
78
+
79
+ if url and key:
80
+ supabase = create_client(url, key)
81
+ supabase.table('models').select('count').execute()
82
+ print("Supabase: connection successful")
83
+ else:
84
+ print("Supabase: skipped (no credentials provided)")
85
+ except Exception as e:
86
+ print(f"Supabase: connection failed - {e}")
87
+
88
+ # Test AWS with provided values or environment
89
+ try:
90
+ import boto3
91
+ access_key = aws_access_key or os.getenv('AWS_ACCESS_KEY_ID')
92
+ secret_key = aws_secret_key or os.getenv('AWS_SECRET_ACCESS_KEY')
93
+
94
+ if access_key and secret_key:
95
+ s3 = boto3.client('s3',
96
+ aws_access_key_id=access_key,
97
+ aws_secret_access_key=secret_key)
98
+ s3.list_buckets()
99
+ print("AWS S3: connection successful")
100
+ else:
101
+ print("AWS S3: skipped (no credentials provided)")
102
+ except Exception as e:
103
+ print(f"AWS S3: connection failed - {e}")