lium-core 0.1.0__tar.gz
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.
- lium_core-0.1.0/.github/workflows/release.yml +56 -0
- lium_core-0.1.0/.gitignore +10 -0
- lium_core-0.1.0/PKG-INFO +7 -0
- lium_core-0.1.0/pyproject.toml +21 -0
- lium_core-0.1.0/src/lium_core/__init__.py +0 -0
- lium_core-0.1.0/src/lium_core/shared_config/__init__.py +9 -0
- lium_core-0.1.0/src/lium_core/shared_config/client.py +54 -0
- lium_core-0.1.0/src/lium_core/shared_config/defaults.py +124 -0
- lium_core-0.1.0/src/lium_core/shared_config/model.py +22 -0
- lium_core-0.1.0/src/lium_core/shared_config/utils.py +13 -0
- lium_core-0.1.0/tests/test_shared_config.py +248 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Build and Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version:
|
|
7
|
+
description: 'Version to release'
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build Python distribution
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v4
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.11'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: |
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
pip install build
|
|
27
|
+
|
|
28
|
+
- name: Build package
|
|
29
|
+
run: python -m build --sdist --wheel --outdir dist/
|
|
30
|
+
|
|
31
|
+
- name: Upload artifact
|
|
32
|
+
uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
approve-and-publish:
|
|
38
|
+
needs: build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
environment: release
|
|
41
|
+
permissions:
|
|
42
|
+
contents: read
|
|
43
|
+
id-token: write
|
|
44
|
+
|
|
45
|
+
steps:
|
|
46
|
+
- name: Download artifact
|
|
47
|
+
uses: actions/download-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: dist
|
|
50
|
+
path: dist/
|
|
51
|
+
|
|
52
|
+
- name: Publish package distributions to PyPI
|
|
53
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
54
|
+
with:
|
|
55
|
+
verbose: true
|
|
56
|
+
print-hash: true
|
lium_core-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "lium-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Shared core library for Lium platform"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"pydantic>=2.0.0",
|
|
8
|
+
"requests>=2.31.0",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/lium_core"]
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.0.0",
|
|
21
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from lium_core.shared_config.client import SharedConfigClient
|
|
2
|
+
from lium_core.shared_config.defaults import DEFAULT_SHARED_CONFIG
|
|
3
|
+
from lium_core.shared_config.model import SharedConfig
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"SharedConfig",
|
|
7
|
+
"SharedConfigClient",
|
|
8
|
+
"DEFAULT_SHARED_CONFIG",
|
|
9
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from lium_core.shared_config.defaults import DEFAULT_SHARED_CONFIG
|
|
8
|
+
from lium_core.shared_config.model import SharedConfig
|
|
9
|
+
from lium_core.shared_config.utils import dict_diff
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SharedConfigClient:
|
|
15
|
+
def __init__(self, api_url: str, refresh_interval: int = 60):
|
|
16
|
+
self._api_url = api_url
|
|
17
|
+
self._refresh_interval = refresh_interval
|
|
18
|
+
self._lock = threading.Lock()
|
|
19
|
+
self._config: SharedConfig = self._fetch() or DEFAULT_SHARED_CONFIG
|
|
20
|
+
self._running = True
|
|
21
|
+
self._thread = threading.Thread(target=self._refresh_loop, daemon=True)
|
|
22
|
+
self._thread.start()
|
|
23
|
+
|
|
24
|
+
def __getattr__(self, name: str):
|
|
25
|
+
"""Delegate attribute access to the underlying frozen SharedConfig."""
|
|
26
|
+
return getattr(self._config, name)
|
|
27
|
+
|
|
28
|
+
def _fetch(self) -> SharedConfig | None:
|
|
29
|
+
"""Fetch config from API, return None on failure."""
|
|
30
|
+
try:
|
|
31
|
+
response = requests.get(self._api_url, timeout=10)
|
|
32
|
+
response.raise_for_status()
|
|
33
|
+
return SharedConfig.model_validate(response.json())
|
|
34
|
+
except Exception:
|
|
35
|
+
logger.warning("Failed to fetch shared config from %s", self._api_url, exc_info=True)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def _refresh_loop(self) -> None:
|
|
39
|
+
"""Background daemon thread: sleep + fetch."""
|
|
40
|
+
logger.info("Started shared config refresh loop")
|
|
41
|
+
while self._running:
|
|
42
|
+
time.sleep(self._refresh_interval)
|
|
43
|
+
new_config = self._fetch()
|
|
44
|
+
with self._lock:
|
|
45
|
+
if new_config and self._config != new_config:
|
|
46
|
+
for change in dict_diff(self._config.model_dump(), new_config.model_dump()):
|
|
47
|
+
logger.info("Config changed %s", change)
|
|
48
|
+
self._config = new_config
|
|
49
|
+
else:
|
|
50
|
+
logger.debug("Shared config unchanged")
|
|
51
|
+
|
|
52
|
+
def stop(self) -> None:
|
|
53
|
+
"""Stop background refresh."""
|
|
54
|
+
self._running = False
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from lium_core.shared_config.model import SharedConfig
|
|
2
|
+
|
|
3
|
+
DEFAULT_SHARED_CONFIG = SharedConfig(
|
|
4
|
+
machine_prices={
|
|
5
|
+
"NVIDIA B300 SXM6 AC": 3.67,
|
|
6
|
+
"NVIDIA B200": 2.99,
|
|
7
|
+
"NVIDIA H200": 1.90,
|
|
8
|
+
"NVIDIA H200 NVL": 1.67,
|
|
9
|
+
"NVIDIA H100 80GB HBM3": 1.26,
|
|
10
|
+
"NVIDIA H100 NVL": 1.11,
|
|
11
|
+
"NVIDIA H100 PCIe": 1.11,
|
|
12
|
+
"NVIDIA H800 80GB HBM3": 0.88,
|
|
13
|
+
"NVIDIA H800 NVL": 0.80,
|
|
14
|
+
"NVIDIA H800 PCIe": 0.80,
|
|
15
|
+
"NVIDIA GeForce RTX 5090": 0.17,
|
|
16
|
+
"NVIDIA GeForce RTX 4090": 0.14,
|
|
17
|
+
"NVIDIA GeForce RTX 4090 D": 0.11,
|
|
18
|
+
"NVIDIA RTX 4000 Ada Generation": 0.16,
|
|
19
|
+
"NVIDIA RTX 6000 Ada Generation": 0.31,
|
|
20
|
+
"NVIDIA RTX PRO 6000 Blackwell Server Edition": 0.77,
|
|
21
|
+
"NVIDIA RTX PRO 6000 Blackwell Workstation Edition": 0.84,
|
|
22
|
+
"NVIDIA L4": 0.11,
|
|
23
|
+
"NVIDIA L40S": 0.34,
|
|
24
|
+
"NVIDIA L40": 0.29,
|
|
25
|
+
"NVIDIA RTX 2000 Ada Generation": 0.07,
|
|
26
|
+
"NVIDIA A100 80GB PCIe": 0.36,
|
|
27
|
+
"NVIDIA A100-SXM4-80GB": 0.43,
|
|
28
|
+
"NVIDIA RTX A6000": 0.24,
|
|
29
|
+
"NVIDIA RTX A5000": 0.16,
|
|
30
|
+
"NVIDIA RTX A4500": 0.13,
|
|
31
|
+
"NVIDIA RTX A4000": 0.12,
|
|
32
|
+
"NVIDIA A40": 0.12,
|
|
33
|
+
"NVIDIA A30": 0.10,
|
|
34
|
+
"NVIDIA GeForce RTX 3090": 0.13,
|
|
35
|
+
},
|
|
36
|
+
required_deposit_amount={
|
|
37
|
+
"NVIDIA B300 SXM6 AC": 0.274,
|
|
38
|
+
"NVIDIA B200": 0.223,
|
|
39
|
+
"NVIDIA H200": 0.158,
|
|
40
|
+
"NVIDIA H200 NVL": 0.131,
|
|
41
|
+
"NVIDIA H100 80GB HBM3": 0.103,
|
|
42
|
+
"NVIDIA H100 NVL": 0.086,
|
|
43
|
+
"NVIDIA H100 PCIe": 0.086,
|
|
44
|
+
"NVIDIA H800 80GB HBM3": 0.051,
|
|
45
|
+
"NVIDIA H800 NVL": 0.045,
|
|
46
|
+
"NVIDIA H800 PCIe": 0.045,
|
|
47
|
+
"NVIDIA GeForce RTX 5090": 0.014,
|
|
48
|
+
"NVIDIA GeForce RTX 4090": 0.010,
|
|
49
|
+
"NVIDIA GeForce RTX 4090 D": 0.008,
|
|
50
|
+
"NVIDIA RTX PRO 6000 Blackwell Server Edition": 0.0425,
|
|
51
|
+
"NVIDIA RTX PRO 6000 Blackwell Workstation Edition": 0.0459,
|
|
52
|
+
"NVIDIA RTX 6000 Ada Generation": 0.017,
|
|
53
|
+
"NVIDIA L4": 0.008,
|
|
54
|
+
"NVIDIA L40S": 0.027,
|
|
55
|
+
"NVIDIA L40": 0.024,
|
|
56
|
+
"NVIDIA A100 80GB PCIe": 0.027,
|
|
57
|
+
"NVIDIA A100-SXM4-80GB": 0.031,
|
|
58
|
+
"NVIDIA RTX A6000": 0.018,
|
|
59
|
+
"NVIDIA RTX A5000": 0.009,
|
|
60
|
+
"NVIDIA RTX A4500": 0.008,
|
|
61
|
+
"NVIDIA RTX A4000": 0.008,
|
|
62
|
+
"NVIDIA GeForce RTX 3090": 0.008,
|
|
63
|
+
},
|
|
64
|
+
gpu_architectures={
|
|
65
|
+
# Blackwell (sm_100/120)
|
|
66
|
+
"NVIDIA B200": {"arch": "blackwell", "min_cuda": 12.8, "compute_cap": "sm_100"},
|
|
67
|
+
"NVIDIA GeForce RTX 5090": {"arch": "blackwell", "min_cuda": 12.8, "compute_cap": "sm_120"},
|
|
68
|
+
# Hopper (sm_90)
|
|
69
|
+
"NVIDIA H100 80GB HBM3": {"arch": "hopper", "min_cuda": 11.8, "optimal_cuda": 12.2, "compute_cap": "sm_90"},
|
|
70
|
+
"NVIDIA H100 NVL": {"arch": "hopper", "min_cuda": 11.8, "optimal_cuda": 12.2, "compute_cap": "sm_90"},
|
|
71
|
+
"NVIDIA H100 PCIe": {"arch": "hopper", "min_cuda": 11.8, "optimal_cuda": 12.2, "compute_cap": "sm_90"},
|
|
72
|
+
"NVIDIA H200": {"arch": "hopper", "min_cuda": 11.8, "optimal_cuda": 12.2, "compute_cap": "sm_90"},
|
|
73
|
+
"NVIDIA H800 80GB HBM3": {"arch": "hopper", "min_cuda": 11.8, "optimal_cuda": 12.2, "compute_cap": "sm_90"},
|
|
74
|
+
"NVIDIA H800 NVL": {"arch": "hopper", "min_cuda": 11.8, "optimal_cuda": 12.2, "compute_cap": "sm_90"},
|
|
75
|
+
"NVIDIA H800 PCIe": {"arch": "hopper", "min_cuda": 11.8, "optimal_cuda": 12.2, "compute_cap": "sm_90"},
|
|
76
|
+
# Ada Lovelace (sm_89)
|
|
77
|
+
"NVIDIA GeForce RTX 4090": {"arch": "ada", "min_cuda": 11.8, "compute_cap": "sm_89"},
|
|
78
|
+
"NVIDIA GeForce RTX 4090 D": {"arch": "ada", "min_cuda": 11.8, "compute_cap": "sm_89"},
|
|
79
|
+
"NVIDIA L4": {"arch": "ada", "min_cuda": 11.8, "compute_cap": "sm_89"},
|
|
80
|
+
"NVIDIA L40": {"arch": "ada", "min_cuda": 11.8, "compute_cap": "sm_89"},
|
|
81
|
+
"NVIDIA L40S": {"arch": "ada", "min_cuda": 11.8, "compute_cap": "sm_89"},
|
|
82
|
+
"NVIDIA RTX 2000 Ada Generation": {"arch": "ada", "min_cuda": 11.8, "compute_cap": "sm_89"},
|
|
83
|
+
"NVIDIA RTX 4000 Ada Generation": {"arch": "ada", "min_cuda": 11.8, "compute_cap": "sm_89"},
|
|
84
|
+
"NVIDIA RTX 6000 Ada Generation": {"arch": "ada", "min_cuda": 11.8, "compute_cap": "sm_89"},
|
|
85
|
+
# Ampere (sm_86)
|
|
86
|
+
"NVIDIA A100 80GB PCIe": {"arch": "ampere", "min_cuda": 11.0, "compute_cap": "sm_86"},
|
|
87
|
+
"NVIDIA A100-SXM4-80GB": {"arch": "ampere", "min_cuda": 11.0, "compute_cap": "sm_86"},
|
|
88
|
+
"NVIDIA A30": {"arch": "ampere", "min_cuda": 11.0, "compute_cap": "sm_86"},
|
|
89
|
+
"NVIDIA A40": {"arch": "ampere", "min_cuda": 11.0, "compute_cap": "sm_86"},
|
|
90
|
+
"NVIDIA RTX A4000": {"arch": "ampere", "min_cuda": 11.0, "compute_cap": "sm_86"},
|
|
91
|
+
"NVIDIA RTX A4500": {"arch": "ampere", "min_cuda": 11.0, "compute_cap": "sm_86"},
|
|
92
|
+
"NVIDIA RTX A5000": {"arch": "ampere", "min_cuda": 11.0, "compute_cap": "sm_86"},
|
|
93
|
+
"NVIDIA RTX A6000": {"arch": "ampere", "min_cuda": 11.0, "compute_cap": "sm_86"},
|
|
94
|
+
"NVIDIA GeForce RTX 3090": {"arch": "ampere", "min_cuda": 11.0, "compute_cap": "sm_86"},
|
|
95
|
+
},
|
|
96
|
+
driver_cuda_map={
|
|
97
|
+
450: 11.0,
|
|
98
|
+
470: 11.4,
|
|
99
|
+
495: 11.5,
|
|
100
|
+
510: 11.6,
|
|
101
|
+
515: 11.7,
|
|
102
|
+
525: 12.0,
|
|
103
|
+
530: 12.1,
|
|
104
|
+
535: 12.2,
|
|
105
|
+
545: 12.3,
|
|
106
|
+
550: 12.4,
|
|
107
|
+
555: 12.5,
|
|
108
|
+
560: 12.6,
|
|
109
|
+
565: 12.7,
|
|
110
|
+
570: 12.8,
|
|
111
|
+
575: 12.8,
|
|
112
|
+
580: 13.0,
|
|
113
|
+
590: 13.1,
|
|
114
|
+
},
|
|
115
|
+
machine_max_price_rate=3.0,
|
|
116
|
+
machine_min_price_rate=0.5,
|
|
117
|
+
rental_fees_rate=0.9,
|
|
118
|
+
collateral_days=7,
|
|
119
|
+
collateral_contract_address="0x7DCCb5659c70Ce2104A9bb79E9E257473ECbe628",
|
|
120
|
+
bittensor_netuid=51,
|
|
121
|
+
volume_gb_hour_price_usd=0.00005,
|
|
122
|
+
max_initial_port_count=200,
|
|
123
|
+
total_burn_emission=0.91,
|
|
124
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SharedConfig(BaseModel):
|
|
5
|
+
model_config = ConfigDict(frozen=True)
|
|
6
|
+
|
|
7
|
+
# Dicts
|
|
8
|
+
machine_prices: dict[str, float]
|
|
9
|
+
required_deposit_amount: dict[str, float]
|
|
10
|
+
gpu_architectures: dict[str, dict]
|
|
11
|
+
driver_cuda_map: dict[int, float]
|
|
12
|
+
|
|
13
|
+
# Scalars
|
|
14
|
+
machine_max_price_rate: float
|
|
15
|
+
machine_min_price_rate: float
|
|
16
|
+
rental_fees_rate: float
|
|
17
|
+
collateral_days: int
|
|
18
|
+
collateral_contract_address: str
|
|
19
|
+
bittensor_netuid: int
|
|
20
|
+
volume_gb_hour_price_usd: float
|
|
21
|
+
max_initial_port_count: int
|
|
22
|
+
total_burn_emission: float
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
def dict_diff(old: dict, new: dict, prefix: str = "") -> list[str]:
|
|
2
|
+
"""Return list of human-readable strings describing differences."""
|
|
3
|
+
changes: list[str] = []
|
|
4
|
+
all_keys = old.keys() | new.keys()
|
|
5
|
+
for key in sorted(all_keys):
|
|
6
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
7
|
+
old_val = old.get(key)
|
|
8
|
+
new_val = new.get(key)
|
|
9
|
+
if isinstance(old_val, dict) and isinstance(new_val, dict):
|
|
10
|
+
changes.extend(dict_diff(old_val, new_val, path))
|
|
11
|
+
elif old_val != new_val:
|
|
12
|
+
changes.append(f"[{path}]: {old_val} -> {new_val}")
|
|
13
|
+
return changes
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pydantic
|
|
5
|
+
import pytest
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from lium_core.shared_config.client import SharedConfigClient
|
|
9
|
+
from lium_core.shared_config.defaults import DEFAULT_SHARED_CONFIG
|
|
10
|
+
from lium_core.shared_config.model import SharedConfig
|
|
11
|
+
from lium_core.shared_config.utils import dict_diff
|
|
12
|
+
|
|
13
|
+
API_URL = "http://fake-api/config"
|
|
14
|
+
|
|
15
|
+
SAMPLE_CONFIG_DATA = DEFAULT_SHARED_CONFIG.model_dump()
|
|
16
|
+
|
|
17
|
+
ALTERED_CONFIG_DATA = {
|
|
18
|
+
**SAMPLE_CONFIG_DATA,
|
|
19
|
+
"rental_fees_rate": 0.75,
|
|
20
|
+
"collateral_days": 14,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _make_response(json_data: dict, status_code: int = 200) -> MagicMock:
|
|
25
|
+
resp = MagicMock(spec=requests.Response)
|
|
26
|
+
resp.status_code = status_code
|
|
27
|
+
resp.json.return_value = json_data
|
|
28
|
+
resp.raise_for_status.return_value = None
|
|
29
|
+
if status_code >= 400:
|
|
30
|
+
resp.raise_for_status.side_effect = requests.HTTPError(response=resp)
|
|
31
|
+
return resp
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_client(mock_get: MagicMock) -> SharedConfigClient:
|
|
35
|
+
"""Create client with patched threading so no background loop runs."""
|
|
36
|
+
with patch("lium_core.shared_config.client.threading"):
|
|
37
|
+
client = SharedConfigClient(api_url=API_URL, refresh_interval=60)
|
|
38
|
+
return client
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ==================== Model tests ====================
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_shared_config_is_frozen() -> None:
|
|
45
|
+
with pytest.raises(pydantic.ValidationError):
|
|
46
|
+
DEFAULT_SHARED_CONFIG.rental_fees_rate = 0.5
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_default_shared_config_has_all_fields() -> None:
|
|
50
|
+
assert len(DEFAULT_SHARED_CONFIG.machine_prices) > 0
|
|
51
|
+
assert len(DEFAULT_SHARED_CONFIG.required_deposit_amount) > 0
|
|
52
|
+
assert len(DEFAULT_SHARED_CONFIG.gpu_architectures) > 0
|
|
53
|
+
assert len(DEFAULT_SHARED_CONFIG.driver_cuda_map) > 0
|
|
54
|
+
assert DEFAULT_SHARED_CONFIG.machine_max_price_rate == 3.0
|
|
55
|
+
assert DEFAULT_SHARED_CONFIG.machine_min_price_rate == 0.5
|
|
56
|
+
assert DEFAULT_SHARED_CONFIG.rental_fees_rate == 0.9
|
|
57
|
+
assert DEFAULT_SHARED_CONFIG.collateral_days == 7
|
|
58
|
+
assert DEFAULT_SHARED_CONFIG.collateral_contract_address == "0x7DCCb5659c70Ce2104A9bb79E9E257473ECbe628"
|
|
59
|
+
assert DEFAULT_SHARED_CONFIG.bittensor_netuid == 51
|
|
60
|
+
assert DEFAULT_SHARED_CONFIG.volume_gb_hour_price_usd == 0.00005
|
|
61
|
+
assert DEFAULT_SHARED_CONFIG.max_initial_port_count == 200
|
|
62
|
+
assert DEFAULT_SHARED_CONFIG.total_burn_emission == 0.91
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_shared_config_serializes_to_json() -> None:
|
|
66
|
+
data = DEFAULT_SHARED_CONFIG.model_dump()
|
|
67
|
+
assert isinstance(data, dict)
|
|
68
|
+
assert "machine_prices" in data
|
|
69
|
+
assert "gpu_architectures" in data
|
|
70
|
+
assert isinstance(data["gpu_architectures"]["NVIDIA B200"]["arch"], str)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ==================== Utils tests (dict_diff) ====================
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.parametrize(
|
|
77
|
+
"old, new, expected",
|
|
78
|
+
[
|
|
79
|
+
pytest.param({}, {}, [], id="both_empty"),
|
|
80
|
+
pytest.param({"a": 1}, {"a": 1}, [], id="no_changes"),
|
|
81
|
+
pytest.param(
|
|
82
|
+
{"a": 1},
|
|
83
|
+
{"a": 2},
|
|
84
|
+
["[a]: 1 -> 2"],
|
|
85
|
+
id="top_level_change",
|
|
86
|
+
),
|
|
87
|
+
pytest.param(
|
|
88
|
+
{"a": {"b": 1}},
|
|
89
|
+
{"a": {"b": 2}},
|
|
90
|
+
["[a.b]: 1 -> 2"],
|
|
91
|
+
id="nested_change",
|
|
92
|
+
),
|
|
93
|
+
pytest.param(
|
|
94
|
+
{"a": {"b": {"c": 1}}},
|
|
95
|
+
{"a": {"b": {"c": 99}}},
|
|
96
|
+
["[a.b.c]: 1 -> 99"],
|
|
97
|
+
id="deep_nested_change",
|
|
98
|
+
),
|
|
99
|
+
pytest.param(
|
|
100
|
+
{},
|
|
101
|
+
{"a": 1},
|
|
102
|
+
["[a]: None -> 1"],
|
|
103
|
+
id="key_added",
|
|
104
|
+
),
|
|
105
|
+
pytest.param(
|
|
106
|
+
{"a": 1},
|
|
107
|
+
{},
|
|
108
|
+
["[a]: 1 -> None"],
|
|
109
|
+
id="key_removed",
|
|
110
|
+
),
|
|
111
|
+
pytest.param(
|
|
112
|
+
{"a": 1, "b": 2, "c": 3},
|
|
113
|
+
{"a": 10, "b": 20, "c": 30},
|
|
114
|
+
["[a]: 1 -> 10", "[b]: 2 -> 20", "[c]: 3 -> 30"],
|
|
115
|
+
id="multiple_changes_sorted",
|
|
116
|
+
),
|
|
117
|
+
pytest.param(
|
|
118
|
+
{"a": {"nested": 1}},
|
|
119
|
+
{"a": "flat"},
|
|
120
|
+
["[a]: {'nested': 1} -> flat"],
|
|
121
|
+
id="dict_becomes_scalar",
|
|
122
|
+
),
|
|
123
|
+
],
|
|
124
|
+
)
|
|
125
|
+
def test_dict_diff(old: dict, new: dict, expected: list[str]) -> None:
|
|
126
|
+
assert dict_diff(old, new) == expected
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ==================== Client tests: _fetch ====================
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_fetch_success() -> None:
|
|
133
|
+
with patch("lium_core.shared_config.client.requests.get", return_value=_make_response(SAMPLE_CONFIG_DATA)):
|
|
134
|
+
client = _build_client(MagicMock())
|
|
135
|
+
|
|
136
|
+
assert isinstance(client._config, SharedConfig)
|
|
137
|
+
assert client._config == DEFAULT_SHARED_CONFIG
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_fetch_http_error() -> None:
|
|
141
|
+
with patch("lium_core.shared_config.client.requests.get", return_value=_make_response({}, status_code=500)):
|
|
142
|
+
client = _build_client(MagicMock())
|
|
143
|
+
|
|
144
|
+
assert client._config == DEFAULT_SHARED_CONFIG
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_fetch_network_error() -> None:
|
|
148
|
+
with patch("lium_core.shared_config.client.requests.get", side_effect=requests.ConnectionError("no network")):
|
|
149
|
+
client = _build_client(MagicMock())
|
|
150
|
+
|
|
151
|
+
assert client._config == DEFAULT_SHARED_CONFIG
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ==================== Client tests: __init__ ====================
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_init_with_successful_fetch() -> None:
|
|
158
|
+
with patch("lium_core.shared_config.client.requests.get", return_value=_make_response(ALTERED_CONFIG_DATA)):
|
|
159
|
+
client = _build_client(MagicMock())
|
|
160
|
+
|
|
161
|
+
assert client._config.rental_fees_rate == 0.75
|
|
162
|
+
assert client._config.collateral_days == 14
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_init_fallback_to_default() -> None:
|
|
166
|
+
with patch("lium_core.shared_config.client.requests.get", side_effect=Exception("boom")):
|
|
167
|
+
client = _build_client(MagicMock())
|
|
168
|
+
|
|
169
|
+
assert client._config is DEFAULT_SHARED_CONFIG
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ==================== Client tests: _refresh_loop ====================
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_refresh_updates_config_on_change() -> None:
|
|
176
|
+
mock_get = MagicMock(return_value=_make_response(SAMPLE_CONFIG_DATA))
|
|
177
|
+
with patch("lium_core.shared_config.client.requests.get", mock_get):
|
|
178
|
+
client = _build_client(mock_get)
|
|
179
|
+
|
|
180
|
+
assert client._config == DEFAULT_SHARED_CONFIG
|
|
181
|
+
|
|
182
|
+
mock_get.return_value = _make_response(ALTERED_CONFIG_DATA)
|
|
183
|
+
|
|
184
|
+
def _stop_after_one_iteration(_interval: int) -> None:
|
|
185
|
+
client._running = False
|
|
186
|
+
|
|
187
|
+
with (
|
|
188
|
+
patch("lium_core.shared_config.client.requests.get", mock_get),
|
|
189
|
+
patch("lium_core.shared_config.client.time.sleep", side_effect=_stop_after_one_iteration),
|
|
190
|
+
):
|
|
191
|
+
client._running = True
|
|
192
|
+
client._refresh_loop()
|
|
193
|
+
|
|
194
|
+
assert client._config.rental_fees_rate == 0.75
|
|
195
|
+
assert client._config.collateral_days == 14
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_refresh_skips_on_same_config(caplog: pytest.LogCaptureFixture) -> None:
|
|
199
|
+
mock_get = MagicMock(return_value=_make_response(SAMPLE_CONFIG_DATA))
|
|
200
|
+
with patch("lium_core.shared_config.client.requests.get", mock_get):
|
|
201
|
+
client = _build_client(mock_get)
|
|
202
|
+
|
|
203
|
+
def _stop_after_one_iteration(_interval: int) -> None:
|
|
204
|
+
client._running = False
|
|
205
|
+
|
|
206
|
+
with (
|
|
207
|
+
patch("lium_core.shared_config.client.requests.get", mock_get),
|
|
208
|
+
patch("lium_core.shared_config.client.time.sleep", side_effect=_stop_after_one_iteration),
|
|
209
|
+
caplog.at_level(logging.DEBUG, logger="lium_core.shared_config.client"),
|
|
210
|
+
):
|
|
211
|
+
client._running = True
|
|
212
|
+
client._refresh_loop()
|
|
213
|
+
|
|
214
|
+
assert "unchanged" in caplog.text
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_refresh_skips_on_fetch_failure() -> None:
|
|
218
|
+
mock_get = MagicMock(return_value=_make_response(SAMPLE_CONFIG_DATA))
|
|
219
|
+
with patch("lium_core.shared_config.client.requests.get", mock_get):
|
|
220
|
+
client = _build_client(mock_get)
|
|
221
|
+
|
|
222
|
+
original_config = client._config
|
|
223
|
+
mock_get.side_effect = requests.ConnectionError("down")
|
|
224
|
+
|
|
225
|
+
def _stop_after_one_iteration(_interval: int) -> None:
|
|
226
|
+
client._running = False
|
|
227
|
+
|
|
228
|
+
with (
|
|
229
|
+
patch("lium_core.shared_config.client.requests.get", mock_get),
|
|
230
|
+
patch("lium_core.shared_config.client.time.sleep", side_effect=_stop_after_one_iteration),
|
|
231
|
+
):
|
|
232
|
+
client._running = True
|
|
233
|
+
client._refresh_loop()
|
|
234
|
+
|
|
235
|
+
assert client._config is original_config
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ==================== Client tests: __getattr__ ====================
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_getattr_delegates_to_config() -> None:
|
|
242
|
+
mock_get = MagicMock(return_value=_make_response(SAMPLE_CONFIG_DATA))
|
|
243
|
+
with patch("lium_core.shared_config.client.requests.get", mock_get):
|
|
244
|
+
client = _build_client(mock_get)
|
|
245
|
+
|
|
246
|
+
assert client.bittensor_netuid == DEFAULT_SHARED_CONFIG.bittensor_netuid
|
|
247
|
+
assert client.rental_fees_rate == DEFAULT_SHARED_CONFIG.rental_fees_rate
|
|
248
|
+
assert client.machine_prices == DEFAULT_SHARED_CONFIG.machine_prices
|