Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions src/aleph/sdk/client/services/crn.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging
from datetime import datetime
from typing import TYPE_CHECKING, Dict, List, Optional, Union

import aiohttp
from aiohttp.client_exceptions import ClientResponseError
from aleph_message.models import ItemHash
from packaging.version import InvalidVersion, Version
from pydantic import BaseModel, NonNegativeInt, PositiveInt
from pydantic import BaseModel, NonNegativeInt, PositiveInt, ValidationError

from aleph.sdk.conf import settings
from aleph.sdk.exceptions import MethodNotAvailableOnCRN, VmNotFoundOnHost
Expand All @@ -22,6 +23,8 @@
if TYPE_CHECKING:
from aleph.sdk.client.http import AlephHttpClient

logger = logging.getLogger(__name__)


class CpuLoad(BaseModel):
load1: float
Expand Down Expand Up @@ -120,10 +123,23 @@ class CrnList(DictLikeModel):
@classmethod
def from_api(cls, payload: dict) -> "CrnList":
raw_list = payload.get("crns", [])
crn_list = [
CRN.model_validate(item) if not isinstance(item, CRN) else item
for item in raw_list
]
crn_list: List[CRN] = []
for item in raw_list:
if isinstance(item, CRN):
crn_list.append(item)
continue
try:
crn_list.append(CRN.model_validate(item))
except ValidationError as exc:
identifier = "<unknown>"
if isinstance(item, dict):
identifier = (
item.get("hash")
or item.get("name")
or item.get("address")
or identifier
)
logger.warning("Skipping malformed CRN %s: %s", identifier, exc)
return cls(crns=crn_list)

def find_gpu_on_network(self):
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,57 @@ def test_crn_version_filter_no_filter_returns_all():

result = crn_list.filter_crn()
assert len(result) == 2


def test_crn_list_from_api_skips_malformed_entries(caplog):
"""One malformed CRN must not crash the whole list (availability hardening)."""
payload = {
"crns": [
{
"hash": "good-crn",
"name": "good",
"address": "0xgood",
"version": "1.0.0",
"payment_receiver_address": None,
},
{
"hash": "bad-crn",
"name": "bad",
"address": "0xbad",
"version": "1.0.0",
"payment_receiver_address": None,
"system_usage": {
"cpu": {
"count": 1,
"load_average": {"load1": 0, "load5": 0, "load15": 0},
"core_frequencies": {"min": 0, "max": 0},
},
"mem": {"total_kB": 1, "available_kB": 0},
"disk": {"total_kB": 1, "available_kB": 0},
"period": {
"start_timestamp": "2026-01-01T00:00:00",
"duration_seconds": 0,
},
"properties": {
"cpu": {"architecture": "x86_64", "vendor": "intel"}
},
"gpu": {"devices": [{"vendor": "NVIDIA"}], "available_devices": []},
"active": True,
},
},
"not-a-dict",
None,
]
}

with caplog.at_level("WARNING"):
result = CrnList.from_api(payload)

assert len(result.crns) == 1
assert result.crns[0].hash == "good-crn"
assert any("bad-crn" in record.message for record in caplog.records)


def test_crn_list_from_api_empty_payload():
assert CrnList.from_api({}).crns == []
assert CrnList.from_api({"crns": []}).crns == []
Loading