golem-vm-provider 0.1.24__py3-none-any.whl → 0.1.27__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.
provider/main.py CHANGED
@@ -1,185 +1,73 @@
1
+ from .api import routes
1
2
  import asyncio
2
3
  import os
4
+ import socket
3
5
  from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
4
7
  from typing import Optional
5
8
 
6
9
  from .config import settings
7
10
  from .utils.logging import setup_logger, PROCESS, SUCCESS
8
11
  from .utils.ascii_art import startup_animation
9
12
  from .discovery.resource_tracker import ResourceTracker
10
- from .discovery.advertiser import ResourceAdvertiser
11
- from .vm.multipass import MultipassProvider
12
- from .vm.port_manager import PortManager
13
-
14
- logger = setup_logger(__name__)
13
+ from .discovery.advertiser import DiscoveryServerAdvertiser
14
+ from .container import Container
15
+ from .service import ProviderService
16
+ from .utils.logging import setup_logger
15
17
 
16
18
  app = FastAPI(title="VM on Golem Provider")
17
-
18
- async def setup_provider() -> None:
19
- """Setup and initialize the provider components."""
20
- try:
21
- # Create resource tracker first
22
- logger.process("🔄 Initializing resource tracker...")
23
- resource_tracker = ResourceTracker()
24
- app.state.resource_tracker = resource_tracker
25
-
26
- # Create provider with resource tracker and temporary port manager
27
- logger.process("🔄 Initializing VM provider...")
28
- provider = MultipassProvider(resource_tracker, port_manager=None) # Will be set later
29
-
30
- try:
31
- # Initialize provider (without port operations)
32
- await asyncio.wait_for(provider.initialize(), timeout=30)
33
-
34
- # Store provider reference
35
- app.state.provider = provider
36
- app.state.proxy_manager = provider.proxy_manager
37
-
38
- # Initialize port manager first to verify all ports
39
- logger.process("🔄 Initializing port manager...")
40
- port_manager = PortManager(
41
- start_port=settings.PORT_RANGE_START,
42
- end_port=settings.PORT_RANGE_END,
43
- discovery_port=settings.PORT
44
- )
45
-
46
- if not await port_manager.initialize():
47
- raise RuntimeError("Port verification failed")
48
-
49
- # Store port manager references
50
- app.state.port_manager = port_manager
51
- provider.port_manager = port_manager
52
- app.state.proxy_manager.port_manager = port_manager
53
-
54
- # Now restore proxy configurations using only verified ports
55
- logger.process("🔄 Restoring proxy configurations...")
56
- await app.state.proxy_manager._load_state()
57
-
58
- except asyncio.TimeoutError:
59
- logger.error("Provider initialization timed out")
60
- raise
61
- except Exception as e:
62
- logger.error(f"Failed to initialize provider: {e}")
63
- raise
64
-
65
- # Create and start advertiser in background
66
- logger.process("🔄 Starting resource advertiser...")
67
- advertiser = ResourceAdvertiser(
68
- resource_tracker=resource_tracker,
69
- discovery_url=settings.DISCOVERY_URL,
70
- provider_id=settings.PROVIDER_ID
71
- )
72
-
73
- # Start advertiser in background task
74
- app.state.advertiser_task = asyncio.create_task(advertiser.start())
75
- app.state.advertiser = advertiser
76
-
77
- logger.success("✨ Provider setup complete and ready to accept requests")
78
- except Exception as e:
79
- logger.error(f"Failed to setup provider: {e}")
80
- # Attempt cleanup of any initialized components
81
- await cleanup_provider()
82
- raise
83
-
84
- async def cleanup_provider() -> None:
85
- """Cleanup provider components."""
86
- cleanup_errors = []
87
-
88
- # Stop advertiser
89
- if hasattr(app.state, "advertiser"):
90
- try:
91
- await app.state.advertiser.stop()
92
- if hasattr(app.state, "advertiser_task"):
93
- app.state.advertiser_task.cancel()
94
- try:
95
- await app.state.advertiser_task
96
- except asyncio.CancelledError:
97
- pass
98
- except Exception as e:
99
- cleanup_errors.append(f"Failed to stop advertiser: {e}")
100
-
101
- # Cleanup proxy manager first to stop all proxy servers
102
- if hasattr(app.state, "proxy_manager"):
103
- try:
104
- await asyncio.wait_for(app.state.proxy_manager.cleanup(), timeout=30)
105
- except asyncio.TimeoutError:
106
- cleanup_errors.append("Proxy manager cleanup timed out")
107
- except Exception as e:
108
- cleanup_errors.append(f"Failed to cleanup proxy manager: {e}")
109
-
110
- # Cleanup provider
111
- if hasattr(app.state, "provider"):
112
- try:
113
- await asyncio.wait_for(app.state.provider.cleanup(), timeout=30)
114
- except asyncio.TimeoutError:
115
- cleanup_errors.append("Provider cleanup timed out")
116
- except Exception as e:
117
- cleanup_errors.append(f"Failed to cleanup provider: {e}")
118
-
119
- if cleanup_errors:
120
- error_msg = "\n".join(cleanup_errors)
121
- logger.error(f"Errors during cleanup:\n{error_msg}")
122
- else:
123
- logger.success("✨ Provider cleanup complete")
19
+ container = Container()
20
+ container.config.from_pydantic(settings)
21
+ app.container = container
22
+ container.wire(modules=[".api.routes"])
23
+
24
+ from .vm.models import VMNotFoundError
25
+ from fastapi import Request
26
+ from fastapi.responses import JSONResponse
27
+
28
+ @app.exception_handler(VMNotFoundError)
29
+ async def vm_not_found_exception_handler(request: Request, exc: VMNotFoundError):
30
+ return JSONResponse(
31
+ status_code=404,
32
+ content={"message": str(exc)},
33
+ )
34
+
35
+ @app.exception_handler(Exception)
36
+ async def generic_exception_handler(request: Request, exc: Exception):
37
+ return JSONResponse(
38
+ status_code=500,
39
+ content={"message": "An unexpected error occurred"},
40
+ )
41
+
42
+ # Add CORS middleware
43
+ app.add_middleware(
44
+ CORSMiddleware,
45
+ allow_origins=["*"], # Allows all origins
46
+ allow_credentials=True,
47
+ allow_methods=["*"], # Allows all methods
48
+ allow_headers=["*"], # Allows all headers
49
+ )
124
50
 
125
51
  @app.on_event("startup")
126
52
  async def startup_event():
127
53
  """Handle application startup."""
128
- try:
129
- # Display startup animation
130
- await startup_animation()
131
-
132
- # Verify ports first
133
- from .vm.port_manager import PortManager
134
- from .utils.port_display import PortVerificationDisplay
135
- from .config import settings
54
+ provider_service = container.provider_service()
55
+ await provider_service.setup(app)
136
56
 
137
- display = PortVerificationDisplay(
138
- provider_port=settings.PORT,
139
- port_range_start=settings.PORT_RANGE_START,
140
- port_range_end=settings.PORT_RANGE_END
141
- )
142
- display.print_header()
143
-
144
- # Initialize port manager
145
- logger.process("🔄 Verifying port accessibility...")
146
- port_manager = PortManager(
147
- start_port=settings.PORT_RANGE_START,
148
- end_port=settings.PORT_RANGE_END,
149
- discovery_port=settings.PORT
150
- )
151
- if not await port_manager.initialize():
152
- logger.error("Port verification failed. Please ensure:")
153
- logger.error(f"1. Port {settings.PORT} is accessible for provider access")
154
- logger.error(f"2. Some ports in range {settings.PORT_RANGE_START}-{settings.PORT_RANGE_END} are accessible for VM access")
155
- logger.error("3. Your firewall/router is properly configured")
156
- raise RuntimeError("Port verification failed")
157
-
158
- logger.success(f"✅ Port verification successful - {len(port_manager.verified_ports)} ports available")
159
-
160
- # Store port manager in app state for later use
161
- app.state.port_manager = port_manager
162
-
163
- # Initialize provider
164
- await setup_provider()
165
- except Exception as e:
166
- logger.error(f"Startup failed: {e}")
167
- # Ensure proper cleanup
168
- await cleanup_provider()
169
- raise
170
57
 
171
58
  @app.on_event("shutdown")
172
59
  async def shutdown_event():
173
60
  """Handle application shutdown."""
174
- await cleanup_provider()
61
+ provider_service = container.provider_service()
62
+ await provider_service.cleanup()
175
63
 
176
64
  # Import routes after app creation to avoid circular imports
177
- from .api import routes
178
65
  app.include_router(routes.router, prefix="/api/v1")
179
66
 
180
67
  # Export app for uvicorn
181
68
  __all__ = ["app", "start"]
182
69
 
70
+
183
71
  def check_requirements():
184
72
  """Check if all requirements are met."""
185
73
  try:
@@ -190,12 +78,13 @@ def check_requirements():
190
78
  logger.error(f"Requirements check failed: {e}")
191
79
  return False
192
80
 
81
+
193
82
  async def verify_provider_port(port: int) -> bool:
194
83
  """Verify that the provider port is available for binding.
195
-
84
+
196
85
  Args:
197
86
  port: The port to verify
198
-
87
+
199
88
  Returns:
200
89
  bool: True if the port is available, False otherwise
201
90
  """
@@ -218,23 +107,47 @@ async def verify_provider_port(port: int) -> bool:
218
107
  logger.error("3. Your firewall allows binding to this port")
219
108
  return False
220
109
 
221
- def start():
110
+
111
+ # The get_local_ip function has been removed as this logic is now handled in config.py
112
+
113
+
114
+ import typer
115
+
116
+ cli = typer.Typer()
117
+
118
+ @cli.command()
119
+ def start(no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification.")):
222
120
  """Start the provider server."""
121
+ run_server(dev_mode=False, no_verify_port=no_verify_port)
122
+
123
+ @cli.command()
124
+ def dev(no_verify_port: bool = typer.Option(True, "--no-verify-port", help="Skip provider port verification.")):
125
+ """Start the provider server in development mode."""
126
+ run_server(dev_mode=True, no_verify_port=no_verify_port)
127
+
128
+ def run_server(dev_mode: bool, no_verify_port: bool):
129
+ """Helper to run the uvicorn server."""
223
130
  import sys
224
131
  from pathlib import Path
225
132
  from dotenv import load_dotenv
226
133
  import uvicorn
227
134
  from .utils.logging import setup_logger
228
- from .config import settings
229
135
 
230
- # Configure logging with debug mode
231
- logger = setup_logger(__name__, debug=True)
136
+ # Load appropriate .env file
137
+ env_file = ".env.dev" if dev_mode else ".env"
138
+ env_path = Path(__file__).parent.parent / env_file
139
+ load_dotenv(dotenv_path=env_path)
232
140
 
141
+ # The logic for setting the public IP in dev mode is now handled in config.py
142
+ # The following lines are no longer needed and have been removed.
143
+
144
+ # Import settings after loading env
145
+ from .config import settings
146
+
147
+ # Configure logging with debug mode
148
+ logger = setup_logger(__name__, debug=dev_mode)
149
+
233
150
  try:
234
- # Load environment variables from .env file
235
- env_path = Path(__file__).parent.parent / '.env'
236
- load_dotenv(dotenv_path=env_path)
237
-
238
151
  # Log environment variables
239
152
  logger.info("Environment variables:")
240
153
  for key, value in os.environ.items():
@@ -245,24 +158,25 @@ def start():
245
158
  if not check_requirements():
246
159
  logger.error("Requirements check failed")
247
160
  sys.exit(1)
248
-
161
+
249
162
  # Verify provider port is available
250
- if not asyncio.run(verify_provider_port(settings.PORT)):
163
+ if not no_verify_port and not asyncio.run(verify_provider_port(settings.PORT)):
251
164
  logger.error(f"Provider port {settings.PORT} is not available")
252
165
  sys.exit(1)
253
-
166
+
254
167
  # Configure uvicorn logging
255
168
  log_config = uvicorn.config.LOGGING_CONFIG
256
169
  log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
257
-
170
+
258
171
  # Run server
259
- logger.process(f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
172
+ logger.process(
173
+ f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
260
174
  uvicorn.run(
261
175
  "provider:app",
262
176
  host=settings.HOST,
263
177
  port=settings.PORT,
264
178
  reload=settings.DEBUG,
265
- log_level="info" if not settings.DEBUG else "debug",
179
+ log_level="debug" if dev_mode else "info",
266
180
  log_config=log_config,
267
181
  timeout_keep_alive=60, # Increase keep-alive timeout
268
182
  limit_concurrency=100, # Limit concurrent connections
@@ -270,3 +184,6 @@ def start():
270
184
  except Exception as e:
271
185
  logger.error(f"Failed to start provider server: {e}")
272
186
  sys.exit(1)
187
+
188
+ if __name__ == "__main__":
189
+ cli()
@@ -13,29 +13,25 @@ class EthereumIdentity:
13
13
  self.key_dir = Path(key_dir)
14
14
  self.key_file = self.key_dir / "provider_key.json"
15
15
 
16
- def get_or_create_identity(self) -> str:
17
- """Get existing provider ID or create new one from Ethereum key."""
18
- # Create directory if it doesn't exist
16
+ def get_or_create_identity(self) -> (str, str):
17
+ """Get existing provider ID and private key, or create a new one."""
19
18
  self.key_dir.mkdir(parents=True, exist_ok=True)
20
- self.key_dir.chmod(0o700) # Secure permissions
21
-
22
- # Check for existing key
19
+ self.key_dir.chmod(0o700)
20
+
23
21
  if self.key_file.exists():
24
- with open(self.key_file) as f:
22
+ with open(self.key_file, "r") as f:
25
23
  key_data = json.load(f)
26
- return key_data["address"]
27
-
28
- # Generate new key
24
+ return key_data["address"], key_data["private_key"]
25
+
29
26
  Account.enable_unaudited_hdwallet_features()
30
27
  acct = Account.create()
31
28
 
32
- # Save key securely
33
29
  key_data = {
34
30
  "address": acct.address,
35
31
  "private_key": acct.key.hex()
36
32
  }
37
33
  with open(self.key_file, "w") as f:
38
34
  json.dump(key_data, f)
39
- self.key_file.chmod(0o600) # Secure permissions
35
+ self.key_file.chmod(0o600)
40
36
 
41
- return acct.address
37
+ return acct.address, acct.key.hex()
@@ -0,0 +1,132 @@
1
+ import asyncio
2
+ import hashlib
3
+ import httpx
4
+ from typing import Optional
5
+
6
+ from golem_base_sdk import GolemBaseClient
7
+ from provider.utils.logging import setup_logger
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+
12
+ class FaucetClient:
13
+ """A client for interacting with a Proof of Work-protected faucet."""
14
+
15
+ def __init__(self, faucet_url: str, captcha_url: str, captcha_api_key: str):
16
+ self.faucet_url = faucet_url
17
+ self.captcha_url = captcha_url
18
+ self.captcha_api_key = captcha_api_key
19
+ self.api_endpoint = f"{faucet_url}/api"
20
+ self.client: Optional[GolemBaseClient] = None
21
+
22
+ async def _ensure_client(self):
23
+ if not self.client:
24
+ from ..config import settings
25
+ private_key_hex = settings.ETHEREUM_PRIVATE_KEY.replace("0x", "")
26
+ private_key_bytes = bytes.fromhex(private_key_hex)
27
+ self.client = await GolemBaseClient.create_ro_client(
28
+ rpc_url=settings.GOLEM_BASE_RPC_URL,
29
+ ws_url=settings.GOLEM_BASE_WS_URL,
30
+ )
31
+
32
+ async def check_balance(self, address: str) -> Optional[float]:
33
+ """Check the balance of the given address."""
34
+ await self._ensure_client()
35
+ try:
36
+ balance_wei = await self.client.http_client().eth.get_balance(address)
37
+ balance_eth = self.client.http_client().from_wei(balance_wei, 'ether')
38
+ return float(balance_eth)
39
+ except Exception as e:
40
+ logger.error(f"Failed to check balance: {e}")
41
+ return None
42
+
43
+ async def get_funds(self, address: str) -> Optional[str]:
44
+ """Request funds from the faucet for the given address."""
45
+ try:
46
+ balance = await self.check_balance(address)
47
+ if balance is not None and balance > 0.01:
48
+ logger.info(f"Sufficient funds ({balance} ETH), skipping faucet request.")
49
+ return None
50
+
51
+ logger.info("Requesting funds from faucet...")
52
+ challenge_data = await self._get_challenge()
53
+ if not challenge_data:
54
+ return None
55
+
56
+ challenge_list = challenge_data.get("challenge")
57
+ token = challenge_data.get("token")
58
+
59
+ if not challenge_list or not token:
60
+ logger.error(f"Invalid challenge data received: {challenge_data}")
61
+ return None
62
+
63
+ solutions = []
64
+ for salt, target in challenge_list:
65
+ nonce = self._solve_challenge(salt, target)
66
+ solutions.append([salt, target, nonce])
67
+
68
+ redeemed_token = await self._redeem_solution(token, solutions)
69
+ if not redeemed_token:
70
+ return None
71
+
72
+ tx_hash = await self._request_faucet(address, redeemed_token)
73
+ if tx_hash:
74
+ logger.success(f"Successfully requested funds. Transaction hash: {tx_hash}")
75
+ return tx_hash
76
+ except Exception as e:
77
+ import traceback
78
+ logger.error(f"Failed to get funds from faucet: {e}")
79
+ logger.error(traceback.format_exc())
80
+ return None
81
+
82
+ async def _get_challenge(self) -> Optional[dict]:
83
+ """Get a PoW challenge from the faucet."""
84
+ try:
85
+ async with httpx.AsyncClient(timeout=60.0) as client:
86
+ url = f"{self.captcha_url}/{self.captcha_api_key}/api/challenge"
87
+ response = await client.post(url)
88
+ response.raise_for_status()
89
+ return response.json()
90
+ except httpx.HTTPStatusError as e:
91
+ logger.error(f"Failed to get PoW challenge: {e.response.text}")
92
+ return None
93
+
94
+ def _solve_challenge(self, salt: str, target: str) -> int:
95
+ """Solve the PoW challenge."""
96
+ target_hash = bytes.fromhex(target)
97
+ nonce = 0
98
+ while True:
99
+ hasher = hashlib.sha256()
100
+ hasher.update(f"{salt}{nonce}".encode())
101
+ if hasher.digest().startswith(target_hash):
102
+ return nonce
103
+ nonce += 1
104
+
105
+ async def _redeem_solution(self, token: str, solutions: list) -> Optional[str]:
106
+ """Redeem the PoW solution to get a CAPTCHA token."""
107
+ try:
108
+ async with httpx.AsyncClient(timeout=60.0) as client:
109
+ url = f"{self.captcha_url}/{self.captcha_api_key}/api/redeem"
110
+ response = await client.post(
111
+ url,
112
+ json={"token": token, "solutions": solutions}
113
+ )
114
+ response.raise_for_status()
115
+ return response.json().get("token")
116
+ except httpx.HTTPStatusError as e:
117
+ logger.error(f"Failed to redeem PoW solution: {e.response.text}")
118
+ return None
119
+
120
+ async def _request_faucet(self, address: str, token: str) -> Optional[str]:
121
+ """Request funds from the faucet with the CAPTCHA token."""
122
+ try:
123
+ async with httpx.AsyncClient(timeout=60.0) as client:
124
+ response = await client.post(
125
+ f"{self.api_endpoint}/faucet",
126
+ json={"address": address, "captchaToken": token}
127
+ )
128
+ response.raise_for_status()
129
+ return response.json().get("txHash")
130
+ except httpx.HTTPStatusError as e:
131
+ logger.error(f"Faucet request failed: {e.response.text}")
132
+ return None
provider/service.py ADDED
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+ from fastapi import FastAPI
3
+
4
+ from .utils.logging import setup_logger
5
+ from .vm.service import VMService
6
+ from .discovery.service import AdvertisementService
7
+
8
+ logger = setup_logger(__name__)
9
+
10
+
11
+ class ProviderService:
12
+ """Service for managing the provider's lifecycle."""
13
+
14
+ def __init__(self, vm_service: VMService, advertisement_service: AdvertisementService, port_manager):
15
+ self.vm_service = vm_service
16
+ self.advertisement_service = advertisement_service
17
+ self.port_manager = port_manager
18
+
19
+ async def setup(self, app: FastAPI):
20
+ """Setup and initialize the provider components."""
21
+ from .config import settings
22
+ from .utils.ascii_art import startup_animation
23
+ from .security.faucet import FaucetClient
24
+
25
+ try:
26
+ # Display startup animation
27
+ await startup_animation()
28
+
29
+ logger.process("🔄 Initializing provider...")
30
+
31
+ # Setup directories
32
+ self._setup_directories()
33
+
34
+ # Initialize services
35
+ await self.port_manager.initialize()
36
+ await self.vm_service.provider.initialize()
37
+ await self.advertisement_service.start()
38
+
39
+ # Check wallet balance and request funds if needed
40
+ faucet_client = FaucetClient(
41
+ faucet_url=settings.FAUCET_URL,
42
+ captcha_url=settings.CAPTCHA_URL,
43
+ captcha_api_key=settings.CAPTCHA_API_KEY,
44
+ )
45
+ await faucet_client.get_funds(settings.PROVIDER_ID)
46
+
47
+ logger.success("✨ Provider setup complete")
48
+ except Exception as e:
49
+ logger.error(f"Startup failed: {e}")
50
+ await self.cleanup()
51
+ raise
52
+
53
+ async def cleanup(self):
54
+ """Cleanup provider components."""
55
+ logger.process("🔄 Cleaning up provider...")
56
+ await self.advertisement_service.stop()
57
+ await self.vm_service.provider.cleanup()
58
+ logger.success("✨ Provider cleanup complete")
59
+
60
+ def _setup_directories(self):
61
+ """Create necessary directories for the provider."""
62
+ from .config import settings
63
+ from pathlib import Path
64
+
65
+ Path(settings.VM_DATA_DIR).mkdir(parents=True, exist_ok=True)
66
+ Path(settings.SSH_KEY_DIR).mkdir(parents=True, exist_ok=True)
67
+ Path(settings.CLOUD_INIT_DIR).mkdir(parents=True, exist_ok=True)
File without changes
provider/utils/logging.py CHANGED
@@ -39,42 +39,26 @@ def setup_logger(name: Optional[str] = None, debug: bool = False) -> logging.Log
39
39
  Configured logger instance
40
40
  """
41
41
  logger = logging.getLogger(name or __name__)
42
- logger.handlers = [] # Clear existing handlers
43
-
44
- # Fancy handler for important logs
45
- fancy_handler = colorlog.StreamHandler(sys.stdout)
46
- fancy_formatter = colorlog.ColoredFormatter(
47
- "%(log_color)s[%(asctime)s] %(message)s",
42
+ if logger.handlers:
43
+ return logger # Already configured
44
+
45
+ handler = colorlog.StreamHandler(sys.stdout)
46
+ formatter = colorlog.ColoredFormatter(
47
+ "%(log_color)s[%(asctime)s] %(levelname)s: %(message)s",
48
48
  datefmt="%Y-%m-%d %H:%M:%S",
49
- reset=True,
50
49
  log_colors={
50
+ 'DEBUG': 'cyan',
51
51
  'INFO': 'green',
52
52
  'PROCESS': 'yellow',
53
53
  'WARNING': 'yellow',
54
54
  'SUCCESS': 'green,bold',
55
55
  'ERROR': 'red',
56
56
  'CRITICAL': 'red,bold',
57
- },
58
- secondary_log_colors={},
59
- style='%'
57
+ }
60
58
  )
61
- fancy_handler.setFormatter(fancy_formatter)
62
- fancy_handler.addFilter(lambda record: record.levelno in [INFO, PROCESS, SUCCESS, WARNING, ERROR, CRITICAL])
63
- logger.addHandler(fancy_handler)
64
-
65
- if debug:
66
- # Debug handler for detailed logs
67
- debug_handler = logging.StreamHandler(sys.stdout)
68
- debug_formatter = logging.Formatter(
69
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
70
- datefmt="%Y-%m-%d %H:%M:%S"
71
- )
72
- debug_handler.setFormatter(debug_formatter)
73
- debug_handler.addFilter(lambda record: record.levelno == DEBUG)
74
- logger.addHandler(debug_handler)
75
- logger.setLevel(logging.DEBUG)
76
- else:
77
- logger.setLevel(logging.INFO)
59
+ handler.setFormatter(formatter)
60
+ logger.addHandler(handler)
61
+ logger.setLevel(logging.DEBUG if debug else logging.INFO)
78
62
 
79
63
  return logger
80
64