request-vm-on-golem 0.1.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.
requestor/config.py ADDED
@@ -0,0 +1,67 @@
1
+ from pathlib import Path
2
+ from typing import Optional, Dict
3
+ from pydantic import BaseSettings, Field
4
+
5
+ class RequestorConfig(BaseSettings):
6
+ """Configuration settings for the requestor node."""
7
+
8
+ class Config:
9
+ env_prefix = "GOLEM_REQUESTOR_"
10
+
11
+ # Environment
12
+ environment: str = Field(
13
+ default="development",
14
+ description="Environment mode: 'development' or 'production'"
15
+ )
16
+
17
+ # Discovery Service
18
+ discovery_url: str = Field(
19
+ default="http://localhost:7465",
20
+ description="URL of the discovery service"
21
+ )
22
+
23
+ # Base Directory
24
+ base_dir: Path = Field(
25
+ default_factory=lambda: Path.home() / ".golem",
26
+ description="Base directory for all Golem requestor files"
27
+ )
28
+
29
+ # SSH Settings
30
+ ssh_key_dir: Path = Field(
31
+ default=None,
32
+ description="Directory for SSH keys. Defaults to {base_dir}/ssh"
33
+ )
34
+
35
+ # Database Settings
36
+ db_path: Path = Field(
37
+ default=None,
38
+ description="Path to SQLite database. Defaults to {base_dir}/vms.db"
39
+ )
40
+
41
+ def __init__(self, **kwargs):
42
+ # Set dependent paths before validation
43
+ if 'ssh_key_dir' not in kwargs:
44
+ base_dir = kwargs.get('base_dir', Path.home() / ".golem")
45
+ kwargs['ssh_key_dir'] = base_dir / "ssh"
46
+ if 'db_path' not in kwargs:
47
+ base_dir = kwargs.get('base_dir', Path.home() / ".golem")
48
+ kwargs['db_path'] = base_dir / "vms.db"
49
+ super().__init__(**kwargs)
50
+
51
+ def get_provider_url(self, ip_address: str) -> str:
52
+ """Get provider API URL.
53
+
54
+ Args:
55
+ ip_address: The IP address of the provider. In development mode, this is ignored
56
+ and localhost is always used.
57
+
58
+ Returns:
59
+ The complete provider URL with protocol and port.
60
+ """
61
+ # In development mode, always use localhost regardless of the ip_address parameter
62
+ if self.environment == "development":
63
+ return "http://localhost:7466"
64
+ # In production mode, use the actual provider IP address
65
+ return f"http://{ip_address}:7466"
66
+
67
+ config = RequestorConfig()
@@ -0,0 +1,5 @@
1
+ """Database management module."""
2
+
3
+ from .sqlite import Database
4
+
5
+ __all__ = ['Database']
requestor/db/sqlite.py ADDED
@@ -0,0 +1,120 @@
1
+ import aiosqlite
2
+ from pathlib import Path
3
+ from typing import Optional, Dict, List
4
+ import json
5
+
6
+ class Database:
7
+ def __init__(self, db_path: Path):
8
+ self.db_path = db_path
9
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
10
+
11
+ async def init(self):
12
+ """Initialize database and handle migrations."""
13
+ async with aiosqlite.connect(self.db_path) as db:
14
+ # Check if table exists
15
+ async with db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='vms'") as cursor:
16
+ table_exists = await cursor.fetchone() is not None
17
+
18
+ if not table_exists:
19
+ # Create new table without ssh_key_name
20
+ await db.execute("""
21
+ CREATE TABLE vms (
22
+ name TEXT PRIMARY KEY,
23
+ provider_ip TEXT NOT NULL,
24
+ vm_id TEXT NOT NULL,
25
+ config TEXT NOT NULL,
26
+ status TEXT NOT NULL DEFAULT 'running',
27
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
28
+ )
29
+ """)
30
+ else:
31
+ # Check if ssh_key_name column exists
32
+ async with db.execute("PRAGMA table_info(vms)") as cursor:
33
+ columns = await cursor.fetchall()
34
+ has_ssh_key_name = any(col[1] == 'ssh_key_name' for col in columns)
35
+
36
+ if has_ssh_key_name:
37
+ # Migrate existing data
38
+ await db.execute("""
39
+ CREATE TABLE vms_new (
40
+ name TEXT PRIMARY KEY,
41
+ provider_ip TEXT NOT NULL,
42
+ vm_id TEXT NOT NULL,
43
+ config TEXT NOT NULL,
44
+ status TEXT NOT NULL DEFAULT 'running',
45
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
46
+ )
47
+ """)
48
+ await db.execute("""
49
+ INSERT INTO vms_new (name, provider_ip, vm_id, config, status, created_at)
50
+ SELECT name, provider_ip, vm_id, config, status, created_at FROM vms
51
+ """)
52
+ await db.execute("DROP TABLE vms")
53
+ await db.execute("ALTER TABLE vms_new RENAME TO vms")
54
+
55
+ await db.commit()
56
+
57
+ async def save_vm(
58
+ self,
59
+ name: str,
60
+ provider_ip: str,
61
+ vm_id: str,
62
+ config: Dict,
63
+ status: str = 'running'
64
+ ) -> None:
65
+ """Save VM details."""
66
+ async with aiosqlite.connect(self.db_path) as db:
67
+ await db.execute(
68
+ """
69
+ INSERT INTO vms (name, provider_ip, vm_id, config, status)
70
+ VALUES (?, ?, ?, ?, ?)
71
+ """,
72
+ (name, provider_ip, vm_id, json.dumps(config), status)
73
+ )
74
+ await db.commit()
75
+
76
+ async def get_vm(self, name: str) -> Optional[Dict]:
77
+ """Get VM details."""
78
+ async with aiosqlite.connect(self.db_path) as db:
79
+ db.row_factory = aiosqlite.Row
80
+ async with db.execute(
81
+ "SELECT * FROM vms WHERE name = ?",
82
+ (name,)
83
+ ) as cursor:
84
+ row = await cursor.fetchone()
85
+ if row:
86
+ vm = dict(row)
87
+ vm['config'] = json.loads(vm['config'])
88
+ return vm
89
+ return None
90
+
91
+ async def delete_vm(self, name: str) -> None:
92
+ """Delete VM details."""
93
+ async with aiosqlite.connect(self.db_path) as db:
94
+ await db.execute(
95
+ "DELETE FROM vms WHERE name = ?",
96
+ (name,)
97
+ )
98
+ await db.commit()
99
+
100
+ async def update_vm_status(self, name: str, status: str) -> None:
101
+ """Update VM status."""
102
+ async with aiosqlite.connect(self.db_path) as db:
103
+ await db.execute(
104
+ "UPDATE vms SET status = ? WHERE name = ?",
105
+ (status, name)
106
+ )
107
+ await db.commit()
108
+
109
+ async def list_vms(self) -> List[Dict]:
110
+ """List all VMs."""
111
+ async with aiosqlite.connect(self.db_path) as db:
112
+ db.row_factory = aiosqlite.Row
113
+ async with db.execute("SELECT * FROM vms") as cursor:
114
+ rows = await cursor.fetchall()
115
+ vms = []
116
+ for row in rows:
117
+ vm = dict(row)
118
+ vm['config'] = json.loads(vm['config'])
119
+ vms.append(vm)
120
+ return vms
requestor/errors.py ADDED
@@ -0,0 +1,29 @@
1
+ class RequestorError(Exception):
2
+ """Base class for requestor errors."""
3
+ pass
4
+
5
+ class ProviderError(RequestorError):
6
+ """Provider communication error."""
7
+ pass
8
+
9
+ class DiscoveryError(RequestorError):
10
+ """Discovery service error."""
11
+ pass
12
+
13
+ class SSHError(RequestorError):
14
+ """SSH-related error."""
15
+ pass
16
+
17
+ class ConfigError(RequestorError):
18
+ """Configuration error."""
19
+ pass
20
+
21
+ class DatabaseError(RequestorError):
22
+ """Database operation error."""
23
+ pass
24
+
25
+ class VMError(RequestorError):
26
+ """VM operation error."""
27
+ def __init__(self, message: str, vm_id: str = None):
28
+ self.vm_id = vm_id
29
+ super().__init__(message)
@@ -0,0 +1,5 @@
1
+ """Provider communication module."""
2
+
3
+ from .client import ProviderClient
4
+
5
+ __all__ = ['ProviderClient']
@@ -0,0 +1,94 @@
1
+ import aiohttp
2
+ from typing import Dict, Optional
3
+ from pathlib import Path
4
+
5
+ class ProviderClient:
6
+ def __init__(self, provider_url: str):
7
+ self.provider_url = provider_url
8
+ self.session = None
9
+
10
+ async def __aenter__(self):
11
+ self.session = aiohttp.ClientSession()
12
+ return self
13
+
14
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
15
+ if self.session:
16
+ await self.session.close()
17
+
18
+ async def create_vm(
19
+ self,
20
+ name: str,
21
+ cpu: int,
22
+ memory: int,
23
+ storage: int,
24
+ ssh_key: str
25
+ ) -> Dict:
26
+ """Create a VM on the provider."""
27
+ async with self.session.post(
28
+ f"{self.provider_url}/api/v1/vms",
29
+ json={
30
+ "name": name,
31
+ "resources": {
32
+ "cpu": cpu,
33
+ "memory": memory,
34
+ "storage": storage
35
+ },
36
+ "ssh_key": ssh_key
37
+ }
38
+ ) as response:
39
+ if not response.ok:
40
+ error_text = await response.text()
41
+ raise Exception(f"Failed to create VM: {error_text}")
42
+ return await response.json()
43
+
44
+ async def add_ssh_key(self, vm_id: str, key: str) -> None:
45
+ """Add SSH key to VM."""
46
+ async with self.session.post(
47
+ f"{self.provider_url}/api/v1/vms/{vm_id}/ssh-keys",
48
+ json={
49
+ "key": key,
50
+ "name": "default"
51
+ }
52
+ ) as response:
53
+ if not response.ok:
54
+ error_text = await response.text()
55
+ raise Exception(f"Failed to add SSH key: {error_text}")
56
+
57
+ async def start_vm(self, vm_id: str) -> Dict:
58
+ """Start a VM."""
59
+ async with self.session.post(
60
+ f"{self.provider_url}/api/v1/vms/{vm_id}/start"
61
+ ) as response:
62
+ if not response.ok:
63
+ error_text = await response.text()
64
+ raise Exception(f"Failed to start VM: {error_text}")
65
+ return await response.json()
66
+
67
+ async def stop_vm(self, vm_id: str) -> Dict:
68
+ """Stop a VM."""
69
+ async with self.session.post(
70
+ f"{self.provider_url}/api/v1/vms/{vm_id}/stop"
71
+ ) as response:
72
+ if not response.ok:
73
+ error_text = await response.text()
74
+ raise Exception(f"Failed to stop VM: {error_text}")
75
+ return await response.json()
76
+
77
+ async def destroy_vm(self, vm_id: str) -> None:
78
+ """Destroy a VM."""
79
+ async with self.session.delete(
80
+ f"{self.provider_url}/api/v1/vms/{vm_id}"
81
+ ) as response:
82
+ if not response.ok:
83
+ error_text = await response.text()
84
+ raise Exception(f"Failed to destroy VM: {error_text}")
85
+
86
+ async def get_vm_access(self, vm_id: str) -> Dict:
87
+ """Get VM access information."""
88
+ async with self.session.get(
89
+ f"{self.provider_url}/api/v1/vms/{vm_id}/access"
90
+ ) as response:
91
+ if not response.ok:
92
+ error_text = await response.text()
93
+ raise Exception(f"Failed to get VM access info: {error_text}")
94
+ return await response.json()
requestor/run.py ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+ from dotenv import load_dotenv
6
+
7
+ from requestor.utils.logging import setup_logger
8
+ from requestor.cli.commands import cli
9
+
10
+ # Configure logging with debug mode from environment variable
11
+ logger = setup_logger(__name__)
12
+
13
+ def check_requirements():
14
+ """Check if all requirements are met."""
15
+ # Check required directories
16
+ ssh_key_dir = os.environ.get(
17
+ 'GOLEM_REQUESTOR_SSH_KEY_DIR',
18
+ str(Path.home() / '.golem' / 'requestor' / 'ssh')
19
+ )
20
+
21
+ try:
22
+ # Create and secure directories
23
+ path = Path(ssh_key_dir)
24
+ path.mkdir(parents=True, exist_ok=True)
25
+ path.chmod(0o700) # Secure permissions for SSH keys
26
+ except Exception as e:
27
+ logger.error(f"Failed to create required directories: {e}")
28
+ return False
29
+
30
+ return True
31
+
32
+ def main():
33
+ """Run the requestor CLI."""
34
+ try:
35
+ # Load environment variables from .env file
36
+ env_path = Path(__file__).parent / '.env'
37
+ load_dotenv(dotenv_path=env_path)
38
+
39
+ # Check requirements
40
+ if not check_requirements():
41
+ logger.error("Requirements check failed")
42
+ sys.exit(1)
43
+
44
+ # Run CLI
45
+ cli()
46
+ except Exception as e:
47
+ logger.error(f"Failed to start requestor CLI: {e}")
48
+ sys.exit(1)
49
+
50
+ if __name__ == "__main__":
51
+ main()
@@ -0,0 +1,4 @@
1
+ """SSH management module for VM on Golem requestor."""
2
+ from .manager import SSHKeyManager
3
+
4
+ __all__ = ['SSHKeyManager']
@@ -0,0 +1,161 @@
1
+ """SSH key management for VM on Golem requestor."""
2
+ import os
3
+ import asyncio
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Tuple, Optional, Union, NamedTuple
8
+ from cryptography.hazmat.primitives import serialization
9
+ from cryptography.hazmat.primitives.asymmetric import rsa
10
+ from cryptography.hazmat.backends import default_backend
11
+
12
+ # Configure basic logging
13
+ logging.basicConfig(
14
+ level=logging.DEBUG,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
16
+ stream=sys.stderr
17
+ )
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class KeyPair(NamedTuple):
21
+ """Represents an SSH key pair with both private and public keys.
22
+
23
+ Attributes:
24
+ private_key: Path to the private key file
25
+ public_key: Path to the public key file
26
+ private_key_content: Content of the private key file
27
+ public_key_content: Content of the public key file
28
+ """
29
+ private_key: Path
30
+ public_key: Path
31
+ private_key_content: str
32
+ public_key_content: str
33
+
34
+ class SSHKeyManager:
35
+ """Manages SSH keys for VM connections."""
36
+
37
+ def __init__(self, golem_dir: Union[str, Path] = None):
38
+ """Initialize SSH key manager.
39
+
40
+ Args:
41
+ golem_dir: Optional custom directory for Golem SSH keys. If None, uses default from config.
42
+ """
43
+ # Set up Golem SSH directory
44
+ if golem_dir is None:
45
+ from ..config import config
46
+ self.golem_dir = Path(config.ssh_key_dir)
47
+ else:
48
+ self.golem_dir = Path(golem_dir)
49
+
50
+ # Define key paths
51
+ self.system_key_path = Path.home() / '.ssh' / 'id_rsa'
52
+ self.golem_key_path = self.golem_dir / 'golem_id_rsa' # Single reusable key
53
+
54
+ # Create Golem directory if needed
55
+ self.golem_dir.mkdir(parents=True, exist_ok=True)
56
+ # Secure directory permissions (on Unix-like systems)
57
+ if os.name == 'posix':
58
+ os.chmod(self.golem_dir, 0o700)
59
+
60
+ async def get_key_pair(self) -> KeyPair:
61
+ """Get the SSH key pair to use.
62
+
63
+ Returns system SSH key if available, otherwise returns/creates Golem key.
64
+ """
65
+ # Try to use system SSH key first
66
+ logger.debug("Checking for system SSH key at %s", self.system_key_path)
67
+ system_pub_key = self.system_key_path.parent / 'id_rsa.pub'
68
+
69
+ if self.system_key_path.exists() and system_pub_key.exists():
70
+ logger.info("Using existing system SSH key")
71
+ try:
72
+ return KeyPair(
73
+ private_key=self.system_key_path,
74
+ public_key=system_pub_key,
75
+ private_key_content=self.system_key_path.read_text().strip(),
76
+ public_key_content=system_pub_key.read_text().strip()
77
+ )
78
+ except (PermissionError, OSError) as e:
79
+ logger.warning("Could not read system SSH key: %s", e)
80
+ # Fall through to use Golem key
81
+
82
+ # Use/create Golem key if system key unavailable
83
+ logger.debug("Using Golem SSH key at %s", self.golem_key_path)
84
+ if not self.golem_key_path.exists():
85
+ logger.info("No existing Golem SSH key found, generating new key pair")
86
+ await self._generate_key_pair()
87
+
88
+ golem_pub_key = Path(str(self.golem_key_path) + '.pub')
89
+ return KeyPair(
90
+ private_key=self.golem_key_path,
91
+ public_key=golem_pub_key,
92
+ private_key_content=self.golem_key_path.read_text().strip(),
93
+ public_key_content=golem_pub_key.read_text().strip()
94
+ )
95
+
96
+ async def get_public_key_content(self) -> str:
97
+ """Get the content of the public key file."""
98
+ key_pair = await self.get_key_pair()
99
+ return key_pair.public_key_content
100
+
101
+ async def get_key_content(self) -> KeyPair:
102
+ """Get both the paths and contents of the key pair."""
103
+ logger.debug("Getting key content")
104
+ key_pair = await self.get_key_pair()
105
+ logger.debug("Got key pair with paths: private=%s, public=%s",
106
+ key_pair.private_key, key_pair.public_key)
107
+ return key_pair
108
+
109
+ @classmethod
110
+ async def generate_key_pair(cls) -> KeyPair:
111
+ """Generate a new RSA key pair for Golem VMs and return their contents."""
112
+ logger.info("Generating new SSH key pair")
113
+ manager = cls()
114
+ await manager._generate_key_pair()
115
+ logger.debug("Key pair generated, getting content")
116
+ return await manager.get_key_content()
117
+
118
+ async def _generate_key_pair(self):
119
+ """Generate a new RSA key pair for Golem VMs."""
120
+ logger.debug("Generating new RSA key pair")
121
+ try:
122
+ # Generate private key
123
+ private_key = rsa.generate_private_key(
124
+ public_exponent=65537,
125
+ key_size=2048,
126
+ backend=default_backend()
127
+ )
128
+ logger.debug("Generated private key")
129
+
130
+ # Save private key
131
+ private_pem = private_key.private_bytes(
132
+ encoding=serialization.Encoding.PEM,
133
+ format=serialization.PrivateFormat.PKCS8,
134
+ encryption_algorithm=serialization.NoEncryption()
135
+ )
136
+ logger.debug("Saving private key to %s", self.golem_key_path)
137
+ self.golem_key_path.write_bytes(private_pem)
138
+ if os.name == 'posix':
139
+ os.chmod(self.golem_key_path, 0o600) # Secure key permissions on Unix-like systems
140
+
141
+ # Save public key
142
+ logger.debug("Generating public key")
143
+ public_key = private_key.public_key()
144
+ public_pem = public_key.public_bytes(
145
+ encoding=serialization.Encoding.OpenSSH,
146
+ format=serialization.PublicFormat.OpenSSH
147
+ )
148
+ pub_key_path = Path(str(self.golem_key_path) + '.pub')
149
+ logger.debug("Saving public key to %s", pub_key_path)
150
+ pub_key_path.write_bytes(public_pem)
151
+ if os.name == 'posix':
152
+ os.chmod(pub_key_path, 0o644) # Public key can be readable on Unix-like systems
153
+ logger.info("Successfully generated and saved SSH key pair")
154
+ except Exception as e:
155
+ logger.error("Failed to generate key pair: %s", str(e))
156
+ raise
157
+
158
+ async def get_private_key_content(self, force_golem_key: bool = False) -> Optional[str]:
159
+ """Get the content of the private key file."""
160
+ key_pair = await self.get_key_pair(force_golem_key)
161
+ return key_pair.private_key_content
@@ -0,0 +1,109 @@
1
+ import logging
2
+ import colorlog
3
+ import sys
4
+ import os
5
+ from typing import Optional
6
+ from enum import Enum
7
+
8
+ # Import standard logging levels
9
+ from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
10
+
11
+ class LogLevel(Enum):
12
+ """Custom log levels for enhanced CLI experience."""
13
+ COMMAND = 15 # For CLI commands
14
+ PROCESS = 25 # For ongoing processes
15
+ SUCCESS = 35 # For successful operations
16
+ DETAIL = 22 # For additional details
17
+
18
+ # Add custom levels to logging
19
+ logging.addLevelName(LogLevel.COMMAND.value, 'COMMAND')
20
+ logging.addLevelName(LogLevel.PROCESS.value, 'PROCESS')
21
+ logging.addLevelName(LogLevel.SUCCESS.value, 'SUCCESS')
22
+ logging.addLevelName(LogLevel.DETAIL.value, 'DETAIL')
23
+
24
+ def command(self, message, *args, **kwargs):
25
+ """Log CLI command with a distinctive style."""
26
+ if self.isEnabledFor(LogLevel.COMMAND.value):
27
+ self._log(LogLevel.COMMAND.value, message, args, **kwargs)
28
+
29
+ def process(self, message, *args, **kwargs):
30
+ """Log ongoing process with a progress indicator."""
31
+ if self.isEnabledFor(LogLevel.PROCESS.value):
32
+ self._log(LogLevel.PROCESS.value, f"⚡ {message}", args, **kwargs)
33
+
34
+ def success(self, message, *args, **kwargs):
35
+ """Log successful operation with a checkmark."""
36
+ if self.isEnabledFor(LogLevel.SUCCESS.value):
37
+ self._log(LogLevel.SUCCESS.value, f"✨ {message}", args, **kwargs)
38
+
39
+ def detail(self, message, *args, **kwargs):
40
+ """Log additional details with an arrow."""
41
+ if self.isEnabledFor(LogLevel.DETAIL.value):
42
+ self._log(LogLevel.DETAIL.value, f" → {message}", args, **kwargs)
43
+
44
+ # Add methods to Logger class
45
+ logging.Logger.command = command
46
+ logging.Logger.process = process
47
+ logging.Logger.success = success
48
+ logging.Logger.detail = detail
49
+
50
+ def setup_logger(name: Optional[str] = None) -> logging.Logger:
51
+ """Setup and return a colored logger optimized for CLI experience.
52
+
53
+ Args:
54
+ name: Logger name (optional)
55
+
56
+ Returns:
57
+ Configured logger instance with fancy formatting
58
+ """
59
+ logger = logging.getLogger(name or __name__)
60
+ logger.handlers = [] # Clear existing handlers
61
+
62
+ # Check DEBUG environment variable
63
+ debug = os.getenv('DEBUG', '').lower() in ('1', 'true', 'yes')
64
+
65
+ # Prevent duplicate logs by removing root handlers
66
+ root = logging.getLogger()
67
+ root.handlers = []
68
+
69
+ # Fancy handler for CLI output
70
+ fancy_handler = colorlog.StreamHandler(sys.stderr) # Use stderr for logs
71
+ fancy_formatter = colorlog.ColoredFormatter(
72
+ "%(log_color)s%(message)s%(reset)s",
73
+ reset=True,
74
+ log_colors={
75
+ 'DEBUG': 'blue',
76
+ 'INFO': 'white',
77
+ 'COMMAND': 'bold_cyan',
78
+ 'DETAIL': 'cyan',
79
+ 'PROCESS': 'yellow',
80
+ 'WARNING': 'yellow',
81
+ 'SUCCESS': 'bold_green',
82
+ 'ERROR': 'bold_red',
83
+ 'CRITICAL': 'bold_red,bg_white',
84
+ },
85
+ secondary_log_colors={},
86
+ style='%'
87
+ )
88
+ fancy_handler.setFormatter(fancy_formatter)
89
+ fancy_handler.addFilter(
90
+ lambda record: record.levelno != DEBUG or debug
91
+ )
92
+ logger.addHandler(fancy_handler)
93
+ logger.propagate = False # Prevent propagation to avoid duplicates
94
+
95
+ if debug:
96
+ logger.setLevel(DEBUG)
97
+ # Enable debug logging for other libraries
98
+ logging.getLogger('asyncio').setLevel(DEBUG)
99
+ logging.getLogger('aiosqlite').setLevel(DEBUG)
100
+ else:
101
+ logger.setLevel(INFO)
102
+ # Suppress debug logs from other libraries
103
+ logging.getLogger('asyncio').setLevel(WARNING)
104
+ logging.getLogger('aiosqlite').setLevel(WARNING)
105
+
106
+ return logger
107
+
108
+ # Create default logger
109
+ logger = setup_logger('golem.requestor')