golem-vm-provider 0.1.56__py3-none-any.whl → 0.1.57__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.56
3
+ Version: 0.1.57
4
4
  Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
5
5
  Keywords: golem,vm,provider,cloud,decentralized
6
6
  Author: Phillip Jensen
@@ -1,9 +1,9 @@
1
1
  provider/__init__.py,sha256=HO1fkPpZqPO3z8O8-eVIyx8xXSMIVuTR_b1YF0RtXOg,45
2
2
  provider/api/__init__.py,sha256=ssX1ugDqEPt8Fn04IymgmG-Ev8PiXLsCSaiZVvHQnec,344
3
3
  provider/api/models.py,sha256=CmfgXqSH3m0HLqY6JvUFI-2IrdGf3EhNKtZ5kbIAX-U,4304
4
- provider/api/routes.py,sha256=RaOhdUZLJVmCHFWHyhYF9kdBmsFSe5rThIYsW6meMrQ,13194
5
- provider/config.py,sha256=IDeAYQ4z8oaT5HcG9jFQhSZrLlLU6wMTGDTbSxK6FSc,28901
6
- provider/container.py,sha256=81x5LiA-qjYN1Uh_JdOxqvuIXiNDr9X3OXNN0VqYFCI,3681
4
+ provider/api/routes.py,sha256=tH6_msflEgx4O6nMku_Lgg4OW-JonqXHv89NibDFc94,13678
5
+ provider/config.py,sha256=nQzYBujgn-Z7Rqh6q0eOsTpk6R9-V-YF1OysmPpSH0Q,28993
6
+ provider/container.py,sha256=xN1a9qClciGomppCBnEGuPPNzGQkYIWlw1lzexrjptM,3726
7
7
  provider/data/deployments/l2.json,sha256=XTNN2C5LkBfp4YbDKdUKfWMdp1fKnfv8D3TgcwVWxtQ,249
8
8
  provider/discovery/__init__.py,sha256=Y6o8RxGevBpuQS3k32y-zSVbP6HBXG3veBl9ElVPKaU,349
9
9
  provider/discovery/advertiser.py,sha256=o-LiDl1j0lXMUU0-zPe3qerjpoD2360EA60Y_V_VeBc,6571
@@ -13,7 +13,7 @@ provider/discovery/multi_advertiser.py,sha256=_J79wA1-XQ4GsLzt9KrKpWigGSGBqtut7D
13
13
  provider/discovery/resource_monitor.py,sha256=AmiEc7yBGEGXCunQ-QKmVgosDX3gOhK1Y58LJZXrwAs,949
14
14
  provider/discovery/resource_tracker.py,sha256=MP7IXd3aIMsjB4xz5Oj9zFDTEnvrnw-Cyxpl33xcJcc,6006
15
15
  provider/discovery/service.py,sha256=vX_mVSxvn3arnb2cKDM_SeJp1ZgPdImP2aUubeXgdRg,915
16
- provider/main.py,sha256=ozyvQc-1oNLznwr09gYdbGBkYncw-1ejdOdNhxOPcTo,52788
16
+ provider/main.py,sha256=2FicpbL8113Gvw3qQzhVHdpYixrNgIYbqwYI0nJaqRI,55746
17
17
  provider/network/port_verifier.py,sha256=mlSzr9Z-W5Z5mL3EYg4zemgGoi8Z5ebNoeFgLGRaoH4,13253
18
18
  provider/payments/blockchain_service.py,sha256=4GrzDKwCSUVoENqjD4RLyJ0qwBOJKMyVk5Li-XNsyTc,3567
19
19
  provider/payments/monitor.py,sha256=seo8vE622IdbcRE3x69IpvHn2mel_tlMNGt_DxOIoww,5386
@@ -24,9 +24,9 @@ provider/security/l2_faucet.py,sha256=yRV4xdPBgU8-LDTLqtuAijfgIoe2kYxvXqJLxFd-BV
24
24
  provider/service.py,sha256=hlQn0woppsYFHZDMEgq-40cOjmiPWruiWLy_dQvaCRU,6859
25
25
  provider/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
27
- provider/utils/logging.py,sha256=VV3oTYSRT8hUejtXLuua1M6kCHmIJgPspIkzsUVhYW0,1920
27
+ provider/utils/logging.py,sha256=1Br806ohJyYpDIw1i8NsNpg8Xc-8-rUYwKBU4LFomLk,2623
28
28
  provider/utils/port_display.py,sha256=u1HWQFA2kPbsM-TnsQfL6Hr4KmjIZWZfsjoxarHpbW0,11981
29
- provider/utils/pricing.py,sha256=uTgiBJ04LuVPKkzwMTVTb6Jb7m_Z3o5XLsoMh49ixqY,6315
29
+ provider/utils/pricing.py,sha256=eDEjt0s7REyTR-7b_3D_a_yPCnQ4req2KvtemYrE2Kw,6673
30
30
  provider/utils/retry.py,sha256=GvBjpr0DpTOgw28M2hI0yt17dpYLRwrxUUqVxWHQPtM,3148
31
31
  provider/utils/setup.py,sha256=Z5dLuBQkb5vdoQsu1HJZwXmu9NWsiBYJ7Vq9-C-_tY8,2932
32
32
  provider/vm/__init__.py,sha256=LJL504QGbqZvBbMN3G9ixMgAwvOWAKW37zUm_EiaW9M,508
@@ -39,7 +39,7 @@ provider/vm/port_manager.py,sha256=iYSwjTjD_ziOhG8aI7juKHw1OwwRUTJQyQoRUNQvz9w,1
39
39
  provider/vm/provider.py,sha256=A7QN89EJjcSS40_SmKeinG1Jp_NGffJaLse-XdKciAs,1164
40
40
  provider/vm/proxy_manager.py,sha256=n4NTsyz2rtrvjtf_ceKBk-g2q_mzqPwruB1q7UlQVBc,14928
41
41
  provider/vm/service.py,sha256=Ki4SGNIZUq3XmaPMwAOoNzdZzKQsmFXid374wgjFPes,4636
42
- golem_vm_provider-0.1.56.dist-info/METADATA,sha256=D83fVjgeQlLJN2XzW_XGGBWK0g0PAe0nOp87hzjgLjU,20932
43
- golem_vm_provider-0.1.56.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
44
- golem_vm_provider-0.1.56.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
45
- golem_vm_provider-0.1.56.dist-info/RECORD,,
42
+ golem_vm_provider-0.1.57.dist-info/METADATA,sha256=qsWTEj2YWwwpv_NTw4f6cFqFulMm5NZvMUP2iAK8-B8,20932
43
+ golem_vm_provider-0.1.57.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
44
+ golem_vm_provider-0.1.57.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
45
+ golem_vm_provider-0.1.57.dist-info/RECORD,,
provider/api/routes.py CHANGED
@@ -7,8 +7,7 @@ from fastapi import APIRouter, HTTPException, Request
7
7
  from dependency_injector.wiring import inject, Provide
8
8
  from fastapi import APIRouter, HTTPException, Depends
9
9
 
10
- from ..config import Settings
11
- from ..config import Settings as _Cfg
10
+ from typing import TYPE_CHECKING, Any
12
11
  from ..container import Container
13
12
  from ..utils.logging import setup_logger
14
13
  from ..utils.ascii_art import vm_creation_animation, vm_status_change
@@ -27,7 +26,7 @@ router = APIRouter()
27
26
  async def create_vm(
28
27
  request: CreateVMRequest,
29
28
  vm_service: VMService = Depends(Provide[Container.vm_service]),
30
- settings: Settings = Depends(Provide[Container.config]),
29
+ settings: Any = Depends(Provide[Container.config]),
31
30
  stream_map = Depends(Provide[Container.stream_map]),
32
31
  ) -> VMInfo:
33
32
  """Create a new VM."""
@@ -39,11 +38,12 @@ async def create_vm(
39
38
  # If payments are enabled, require a valid stream before starting
40
39
  # Determine if we should enforce gating
41
40
  enforce = False
42
- spa = settings["STREAM_PAYMENT_ADDRESS"]
41
+ spa = (settings.get("STREAM_PAYMENT_ADDRESS") if isinstance(settings, dict) else getattr(settings, "STREAM_PAYMENT_ADDRESS", None))
43
42
  if spa and spa != "0x0000000000000000000000000000000000000000":
44
43
  if os.environ.get("PYTEST_CURRENT_TEST"):
45
44
  # In pytest, skip gating only when using default deployment address
46
45
  try:
46
+ from ..config import Settings as _Cfg # type: ignore
47
47
  default_spa, _ = _Cfg._load_l2_deployment() # type: ignore[attr-defined]
48
48
  except Exception:
49
49
  default_spa = None
@@ -54,8 +54,10 @@ async def create_vm(
54
54
  if enforce:
55
55
  if request.stream_id is None:
56
56
  raise HTTPException(status_code=400, detail="stream_id required when payments are enabled")
57
- reader = StreamPaymentReader(settings["POLYGON_RPC_URL"], settings["STREAM_PAYMENT_ADDRESS"])
58
- ok, reason = reader.verify_stream(int(request.stream_id), settings["PROVIDER_ID"])
57
+ rpc_url = settings.get("POLYGON_RPC_URL") if isinstance(settings, dict) else getattr(settings, "POLYGON_RPC_URL", None)
58
+ reader = StreamPaymentReader(rpc_url, spa)
59
+ expected_recipient = settings.get("PROVIDER_ID") if isinstance(settings, dict) else getattr(settings, "PROVIDER_ID", None)
60
+ ok, reason = reader.verify_stream(int(request.stream_id), expected_recipient)
59
61
  try:
60
62
  s = reader.get_stream(int(request.stream_id))
61
63
  now = int(reader.web3.eth.get_block("latest")["timestamp"]) # type: ignore[attr-defined]
@@ -73,7 +75,7 @@ async def create_vm(
73
75
  # Create VM config
74
76
  config = VMConfig(
75
77
  name=request.name,
76
- image=request.image or settings["DEFAULT_VM_IMAGE"],
78
+ image=request.image or (settings.get("DEFAULT_VM_IMAGE") if isinstance(settings, dict) else getattr(settings, "DEFAULT_VM_IMAGE", "")),
77
79
  resources=resources,
78
80
  ssh_key=request.ssh_key
79
81
  )
@@ -143,7 +145,7 @@ async def get_vm_status(
143
145
  async def get_vm_access(
144
146
  requestor_name: str,
145
147
  vm_service: VMService = Depends(Provide[Container.vm_service]),
146
- settings: Settings = Depends(Provide[Container.config]),
148
+ settings: Any = Depends(Provide[Container.config]),
147
149
  ) -> VMAccessInfo:
148
150
  """Get VM access information."""
149
151
  try:
@@ -156,7 +158,7 @@ async def get_vm_access(
156
158
  raise HTTPException(404, "VM mapping not found")
157
159
 
158
160
  return VMAccessInfo(
159
- ssh_host=settings["PUBLIC_IP"] or "localhost",
161
+ ssh_host=((settings.get("PUBLIC_IP") if isinstance(settings, dict) else getattr(settings, "PUBLIC_IP", None)) or "localhost"),
160
162
  ssh_port=vm.ssh_port,
161
163
  vm_id=requestor_name,
162
164
  multipass_name=multipass_name
@@ -222,7 +224,7 @@ async def delete_vm(
222
224
  raise HTTPException(status_code=500, detail="An unexpected error occurred")
223
225
  @router.get("/provider/info", response_model=ProviderInfoResponse)
224
226
  @inject
225
- async def provider_info(settings: Settings = Depends(Provide[Container.config])) -> ProviderInfoResponse:
227
+ async def provider_info(settings: Any = Depends(Provide[Container.config])) -> ProviderInfoResponse:
226
228
  return ProviderInfoResponse(
227
229
  provider_id=settings["PROVIDER_ID"],
228
230
  stream_payment_address=settings["STREAM_PAYMENT_ADDRESS"],
@@ -234,7 +236,7 @@ async def provider_info(settings: Settings = Depends(Provide[Container.config]))
234
236
  @inject
235
237
  async def get_vm_stream_status(
236
238
  requestor_name: str,
237
- settings: Settings = Depends(Provide[Container.config]),
239
+ settings: Any = Depends(Provide[Container.config]),
238
240
  stream_map = Depends(Provide[Container.stream_map]),
239
241
  ) -> StreamStatus:
240
242
  """Return on-chain stream status for a VM (if mapped)."""
@@ -266,7 +268,7 @@ async def get_vm_stream_status(
266
268
  @router.get("/payments/streams", response_model=List[StreamStatus])
267
269
  @inject
268
270
  async def list_stream_statuses(
269
- settings: Settings = Depends(Provide[Container.config]),
271
+ settings: Any = Depends(Provide[Container.config]),
270
272
  stream_map = Depends(Provide[Container.stream_map]),
271
273
  ) -> List[StreamStatus]:
272
274
  """List stream status for all mapped VMs."""
provider/config.py CHANGED
@@ -38,7 +38,8 @@ def ensure_config() -> None:
38
38
  created = True
39
39
 
40
40
  if created:
41
- print("Using default settings run with --help to customize")
41
+ # Inform the user, but write to stderr so JSON outputs on stdout remain clean
42
+ logger.info("Using default settings – run with --help to customize")
42
43
 
43
44
 
44
45
  if not os.environ.get("GOLEM_PROVIDER_SKIP_BOOTSTRAP") and not os.environ.get("PYTEST_CURRENT_TEST"):
provider/container.py CHANGED
@@ -2,7 +2,6 @@ import os
2
2
  from dependency_injector import containers, providers
3
3
  from pathlib import Path
4
4
 
5
- from .config import settings
6
5
  from .discovery.resource_tracker import ResourceTracker
7
6
  from .discovery.golem_base_advertiser import GolemBaseAdvertiser
8
7
  from .discovery.advertiser import DiscoveryServerAdvertiser
@@ -49,12 +48,12 @@ class Container(containers.DeclarativeContainer):
49
48
 
50
49
  vm_name_mapper = providers.Singleton(
51
50
  VMNameMapper,
52
- db_path=Path(settings.VM_DATA_DIR) / "vm_names.json",
51
+ db_path=providers.Callable(lambda base: Path(base) / "vm_names.json", config.VM_DATA_DIR),
53
52
  )
54
53
 
55
54
  stream_map = providers.Singleton(
56
55
  StreamMap,
57
- storage_path=Path(settings.VM_DATA_DIR) / "streams.json",
56
+ storage_path=providers.Callable(lambda base: Path(base) / "streams.json", config.VM_DATA_DIR),
58
57
  )
59
58
 
60
59
  port_manager = providers.Singleton(
provider/main.py CHANGED
@@ -1,32 +1,43 @@
1
1
  import asyncio
2
2
  import os
3
+ import sys as _sys
3
4
  import socket
4
5
  from fastapi import FastAPI
5
6
  from fastapi.middleware.cors import CORSMiddleware
6
7
  from typing import Optional
7
8
 
8
- from .config import settings, ensure_config
9
9
  from .utils.logging import setup_logger
10
- from .utils.ascii_art import startup_animation
11
- from .discovery.resource_tracker import ResourceTracker
12
- from .discovery.advertiser import DiscoveryServerAdvertiser
10
+
11
+
12
+ # If the invocation includes --json, mute logs as early as possible
13
+ if "--json" in _sys.argv:
14
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
15
+
16
+ # Defer heavy local imports (may import config) until after we decide on silence
13
17
  from .container import Container
14
18
  from .service import ProviderService
15
19
 
16
-
17
20
  logger = setup_logger(__name__)
18
21
 
19
22
  app = FastAPI(title="VM on Golem Provider")
20
23
  container = Container()
21
- # Load configuration using a dict to avoid version-specific adapters
22
- try:
23
- container.config.from_dict(settings.model_dump())
24
- except Exception:
25
- # Fallback for environments without pydantic v2 model_dump
26
- container.config.from_pydantic(settings)
27
24
  app.container = container
28
25
  container.wire(modules=[".api.routes"])
29
26
 
27
+ # Minimal safe defaults so DI providers that rely on config have paths before runtime
28
+ try:
29
+ from pathlib import Path as _Path
30
+ container.config.from_dict({
31
+ "VM_DATA_DIR": str(_Path.home() / ".golem" / "provider" / "vms"),
32
+ "PROXY_STATE_DIR": str(_Path.home() / ".golem" / "provider" / "proxy"),
33
+ "PORT_RANGE_START": 50800,
34
+ "PORT_RANGE_END": 50900,
35
+ "PORT": 7466,
36
+ "SKIP_PORT_VERIFICATION": True,
37
+ })
38
+ except Exception:
39
+ pass
40
+
30
41
  from .vm.models import VMNotFoundError
31
42
  from fastapi import Request
32
43
  from fastapi.responses import JSONResponse
@@ -57,6 +68,13 @@ app.add_middleware(
57
68
  @app.on_event("startup")
58
69
  async def startup_event():
59
70
  """Handle application startup."""
71
+ # Load configuration into container lazily at runtime
72
+ from .config import settings as _settings
73
+ try:
74
+ container.config.from_dict(_settings.model_dump())
75
+ except Exception:
76
+ # Fallback for environments without pydantic v2 model_dump
77
+ container.config.from_pydantic(_settings)
60
78
  provider_service = container.provider_service()
61
79
  await provider_service.setup(app)
62
80
 
@@ -136,9 +154,8 @@ config_app = typer.Typer(help="Configure stream monitoring and withdrawals")
136
154
  cli.add_typer(config_app, name="config")
137
155
 
138
156
  @cli.callback()
139
- def main():
157
+ def main(ctx: typer.Context):
140
158
  """VM on Golem Provider CLI"""
141
- ensure_config()
142
159
  # No-op callback to initialize config; avoid custom --version flag to keep help stable
143
160
  return
144
161
 
@@ -173,15 +190,24 @@ def status(json_out: bool = typer.Option(False, "--json", help="Output machine-r
173
190
  from rich.panel import Panel
174
191
  from rich import box
175
192
 
176
- # Temporarily quiet info logs during checks for cleaner UI
193
+ # For JSON, set a process-wide mute env that setup_logger() respects
194
+ import os as _os
195
+ if json_out:
196
+ _os.environ["GOLEM_SILENCE_LOGS"] = "1"
197
+
198
+ # Temporarily quiet logs; when --json, suppress near everything
177
199
  prev_level = _logger.level
200
+ import logging as _logging
201
+ _root_logger = _logging.getLogger()
202
+ _prev_root_level = _root_logger.level
178
203
  try:
179
204
  _logger.setLevel("WARNING")
205
+ if json_out:
206
+ _root_logger.setLevel(_logging.CRITICAL)
180
207
  except Exception:
181
208
  pass
182
209
 
183
210
  # Silence port_verifier warnings during status checks for clean UI
184
- import logging as _logging
185
211
  try:
186
212
  _pv_logger = _logging.getLogger("provider.network.port_verifier")
187
213
  _prev_pv_level = _pv_logger.level
@@ -507,6 +533,22 @@ def status(json_out: bool = typer.Option(False, "--json", help="Output machine-r
507
533
  _cfg_logger.setLevel(_prev_cfg_level)
508
534
  except Exception:
509
535
  pass
536
+ # Restore root logger
537
+ try:
538
+ _root_logger.setLevel(_prev_root_level)
539
+ except Exception:
540
+ pass
541
+ # Restore root logger
542
+ try:
543
+ _root_logger.setLevel(_prev_root_level)
544
+ except Exception:
545
+ pass
546
+ # Unset mute env if we set it
547
+ if json_out:
548
+ try:
549
+ del _os.environ["GOLEM_SILENCE_LOGS"]
550
+ except Exception:
551
+ pass
510
552
  return
511
553
 
512
554
  console = Console()
@@ -675,6 +717,16 @@ def status(json_out: bool = typer.Option(False, "--json", help="Output machine-r
675
717
  _cfg_logger.setLevel(_prev_cfg_level)
676
718
  except Exception:
677
719
  pass
720
+ try:
721
+ _root_logger.setLevel(_prev_root_level)
722
+ except Exception:
723
+ pass
724
+ # Unset mute env if we set it
725
+ if json_out:
726
+ try:
727
+ del _os.environ["GOLEM_SILENCE_LOGS"]
728
+ except Exception:
729
+ pass
678
730
 
679
731
 
680
732
  @wallet_app.command("faucet-l2")
@@ -712,8 +764,13 @@ def streams_list(json_out: bool = typer.Option(False, "--json", help="Output in
712
764
  from web3 import Web3
713
765
  import json as _json
714
766
  try:
767
+ if json_out:
768
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
715
769
  if not settings.STREAM_PAYMENT_ADDRESS or settings.STREAM_PAYMENT_ADDRESS == "0x0000000000000000000000000000000000000000":
716
- print("Streaming payments are disabled on this provider.")
770
+ if json_out:
771
+ print(_json.dumps({"error": "streaming_disabled"}, indent=2))
772
+ else:
773
+ print("Streaming payments are disabled on this provider.")
717
774
  raise typer.Exit(code=1)
718
775
  c = Container()
719
776
  c.config.from_pydantic(settings)
@@ -813,8 +870,17 @@ def streams_list(json_out: bool = typer.Option(False, "--json", help="Output in
813
870
  for row in table_rows:
814
871
  print(fmt_row(row))
815
872
  except Exception as e:
816
- print(f"Error: {e}")
873
+ if json_out:
874
+ print(_json.dumps({"error": str(e)}, indent=2))
875
+ else:
876
+ print(f"Error: {e}")
817
877
  raise typer.Exit(code=1)
878
+ finally:
879
+ if json_out:
880
+ try:
881
+ del os.environ["GOLEM_SILENCE_LOGS"]
882
+ except Exception:
883
+ pass
818
884
 
819
885
 
820
886
  @streams_app.command("show")
@@ -828,12 +894,17 @@ def streams_show(vm_id: str = typer.Argument(..., help="VM id (requestor_name)")
828
894
  from web3 import Web3
829
895
  import json as _json
830
896
  try:
897
+ if json_out:
898
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
831
899
  c = Container()
832
900
  c.config.from_pydantic(settings)
833
901
  stream_map = c.stream_map()
834
902
  sid = asyncio.run(stream_map.get(vm_id))
835
903
  if sid is None:
836
- print("No stream mapped for this VM.")
904
+ if json_out:
905
+ print(_json.dumps({"error": "no_stream_mapping", "vm_id": vm_id}, indent=2))
906
+ else:
907
+ print("No stream mapped for this VM.")
837
908
  raise typer.Exit(code=1)
838
909
  reader = StreamPaymentReader(settings.POLYGON_RPC_URL, settings.STREAM_PAYMENT_ADDRESS)
839
910
  s = reader.get_stream(int(sid))
@@ -896,8 +967,17 @@ def streams_show(vm_id: str = typer.Argument(..., help="VM id (requestor_name)")
896
967
  print(" ".join("-" * wi for wi in w))
897
968
  print(" ".join(str(cols[i]).ljust(w[i]) for i in range(len(w))))
898
969
  except Exception as e:
899
- print(f"Error: {e}")
970
+ if json_out:
971
+ print(_json.dumps({"error": str(e), "vm_id": vm_id}, indent=2))
972
+ else:
973
+ print(f"Error: {e}")
900
974
  raise typer.Exit(code=1)
975
+ finally:
976
+ if json_out:
977
+ try:
978
+ del os.environ["GOLEM_SILENCE_LOGS"]
979
+ except Exception:
980
+ pass
901
981
 
902
982
  @streams_app.command("earnings")
903
983
  def streams_earnings(json_out: bool = typer.Option(False, "--json", help="Output in JSON")):
@@ -910,6 +990,8 @@ def streams_earnings(json_out: bool = typer.Option(False, "--json", help="Output
910
990
  from web3 import Web3
911
991
  import json as _json
912
992
  try:
993
+ if json_out:
994
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
913
995
  c = Container()
914
996
  c.config.from_pydantic(settings)
915
997
  stream_map = c.stream_map()
@@ -1001,8 +1083,20 @@ def streams_earnings(json_out: bool = typer.Option(False, "--json", help="Output
1001
1083
  for row in table:
1002
1084
  print(" ".join(str(row[i]).ljust(w2[i]) for i in range(4)))
1003
1085
  except Exception as e:
1004
- print(f"Error: {e}")
1086
+ if json_out:
1087
+ try:
1088
+ print(_json.dumps({"error": str(e)}, indent=2))
1089
+ except Exception:
1090
+ print("{\"error\": \"unexpected\"}")
1091
+ else:
1092
+ print(f"Error: {e}")
1005
1093
  raise typer.Exit(code=1)
1094
+ finally:
1095
+ if json_out:
1096
+ try:
1097
+ del os.environ["GOLEM_SILENCE_LOGS"]
1098
+ except Exception:
1099
+ pass
1006
1100
 
1007
1101
 
1008
1102
  @streams_app.command("withdraw")
provider/utils/logging.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import colorlog
3
3
  import sys
4
+ import os
4
5
  from typing import Optional
5
6
 
6
7
  # Import standard logging levels
@@ -39,10 +40,23 @@ def setup_logger(name: Optional[str] = None, debug: bool = False) -> logging.Log
39
40
  Configured logger instance
40
41
  """
41
42
  logger = logging.getLogger(name or __name__)
43
+
44
+ # Global hard mute for JSON commands or other machine output scenarios
45
+ silence = os.getenv("GOLEM_SILENCE_LOGS", "").lower() in ("1", "true", "yes")
46
+
47
+ # If already configured, still adjust level according to silence/debug
42
48
  if logger.handlers:
43
- return logger # Already configured
49
+ target_level = logging.CRITICAL if silence else (logging.DEBUG if debug else logging.INFO)
50
+ logger.setLevel(target_level)
51
+ for h in logger.handlers:
52
+ try:
53
+ h.setLevel(target_level)
54
+ except Exception:
55
+ pass
56
+ return logger # Already configured (levels updated)
44
57
 
45
- handler = colorlog.StreamHandler(sys.stdout)
58
+ # Send logs to stderr so stdout can be reserved for machine output (e.g., --json)
59
+ handler = colorlog.StreamHandler(sys.stderr)
46
60
  formatter = colorlog.ColoredFormatter(
47
61
  "%(log_color)s[%(asctime)s] %(levelname)s: %(message)s",
48
62
  datefmt="%Y-%m-%d %H:%M:%S",
@@ -58,7 +72,8 @@ def setup_logger(name: Optional[str] = None, debug: bool = False) -> logging.Log
58
72
  )
59
73
  handler.setFormatter(formatter)
60
74
  logger.addHandler(handler)
61
- logger.setLevel(logging.DEBUG if debug else logging.INFO)
75
+ # Apply level based on silence/debug
76
+ logger.setLevel(logging.CRITICAL if silence else (logging.DEBUG if debug else logging.INFO))
62
77
 
63
78
  return logger
64
79
 
provider/utils/pricing.py CHANGED
@@ -7,11 +7,15 @@ import time
7
7
  import requests
8
8
 
9
9
  from ..vm.models import VMResources
10
- from ..config import settings
11
10
  from .logging import setup_logger
12
11
 
13
12
  logger = setup_logger(__name__)
14
13
 
14
+ def _get_settings():
15
+ # Lazy import to avoid side effects during module import (e.g., JSON CLI quieting)
16
+ from ..config import settings as _s
17
+ return _s
18
+
15
19
  # Increase precision for financial calcs
16
20
  getcontext().prec = 28
17
21
 
@@ -21,6 +25,7 @@ def quantize_money(value: Decimal) -> Decimal:
21
25
 
22
26
 
23
27
  def _coingecko_simple_price(ids: str) -> Optional[Decimal]:
28
+ settings = _get_settings()
24
29
  base = settings.COINGECKO_API_URL.rstrip("/")
25
30
  url = f"{base}/simple/price"
26
31
  try:
@@ -44,6 +49,7 @@ def fetch_glm_usd_price() -> Optional[Decimal]:
44
49
 
45
50
  Tries multiple IDs to hedge against slug changes.
46
51
  """
52
+ settings = _get_settings()
47
53
  return _coingecko_simple_price(settings.COINGECKO_IDS)
48
54
 
49
55
 
@@ -70,6 +76,7 @@ def calculate_monthly_cost(resources: VMResources) -> Decimal:
70
76
 
71
77
  Uses the GLM-denominated price-per-unit values configured in settings.
72
78
  """
79
+ settings = _get_settings()
73
80
  core_price = Decimal(str(settings.PRICE_GLM_PER_CORE_MONTH))
74
81
  ram_price = Decimal(str(settings.PRICE_GLM_PER_GB_RAM_MONTH))
75
82
  storage_price = Decimal(str(settings.PRICE_GLM_PER_GB_STORAGE_MONTH))
@@ -98,6 +105,7 @@ def update_glm_unit_prices_from_usd(glm_usd: Decimal) -> Tuple[Decimal, Decimal,
98
105
 
99
106
  Returns a tuple of (core_glm, ram_glm, storage_glm).
100
107
  """
108
+ settings = _get_settings()
101
109
  core_usd = Decimal(str(settings.PRICE_USD_PER_CORE_MONTH))
102
110
  ram_usd = Decimal(str(settings.PRICE_USD_PER_GB_RAM_MONTH))
103
111
  storage_usd = Decimal(str(settings.PRICE_USD_PER_GB_STORAGE_MONTH))
@@ -107,6 +115,7 @@ def update_glm_unit_prices_from_usd(glm_usd: Decimal) -> Tuple[Decimal, Decimal,
107
115
  storage_glm = usd_to_glm(storage_usd, glm_usd)
108
116
 
109
117
  # Persist on settings instance (in-memory)
118
+ settings = _get_settings()
110
119
  settings.PRICE_GLM_PER_CORE_MONTH = float(core_glm)
111
120
  settings.PRICE_GLM_PER_GB_RAM_MONTH = float(ram_glm)
112
121
  settings.PRICE_GLM_PER_GB_STORAGE_MONTH = float(storage_glm)
@@ -129,6 +138,7 @@ class PricingAutoUpdater:
129
138
  self._last_price: Optional[Decimal] = None
130
139
 
131
140
  async def start(self):
141
+ settings = _get_settings()
132
142
  if not settings.PRICING_UPDATE_ENABLED:
133
143
  return
134
144
 
@@ -173,6 +183,7 @@ class PricingAutoUpdater:
173
183
  self._last_price = new_price
174
184
  return True
175
185
  delta = abs((new_price - old) / old) * Decimal("100")
186
+ settings = _get_settings()
176
187
  if delta >= Decimal(str(settings.PRICING_UPDATE_MIN_DELTA_PERCENT)):
177
188
  self._last_price = new_price
178
189
  return True