Skip to content

networking.node.profile

profile

Classes:

Name Description
NodeProfile

Profile information for a node.

Classes

NodeProfile

NodeProfile(static: dict, dynamic: dict, cv: dict)

Profile information for a node.

Methods:

Name Description
from_dict

Factory method to create a NodeProfile instance from a dictionary

check_and_update_specs

Checks current specs against saved specs. Updates profile data.

Source code in unaiverse/networking/node/profile.py
def __init__(self,
             static: dict,
             dynamic: dict,
             cv: dict):

    # Checking provided data
    if not static:
        raise ValueError("Missing static profile data")

    # Forcing key order (important! otherwise the hash operation will not be consistent with the one on the server)
    cv = [{k: _cv[k] for k in sorted(_cv)} for _cv in cv]

    self._profile_data = \
        {
            'static': {
                'node_id': None,
                'node_type': None,
                'node_name': None,
                'node_description': None,
                'created_utc': None,
                'name': None,
                'surname': None,
                'title': None,
                'organization': None,
                'email': None,
                'max_nr_connections': None,
                'allowed_node_ids': None,
                'world_masters_node_ids': None,
                'certified': None,
                'inspector_node_id': None
            },
            'dynamic': {
                'os': None,
                'cpu_cores': None,
                'logical_cpus': None,
                'memory_gb': None,
                'memory_avail': None,
                'memory_used': None,
                'timestamp': None,
                'public_ip_address': None,
                'guessed_location': None,
                'peer_id': None,
                'peer_addresses': None,
                'private_peer_id': None,
                'private_peer_addresses': None,
                'proc_inputs': None,
                'proc_outputs': None,
                'streams': None,
                'connections': {
                    'public_agents': None,  # List of dict
                    'world_agents': None,  # List of dict
                    'world_masters': None,  # List of dict
                    'world_peer_id': None,  # Str
                    'role': None  # Str
                },
                'world_summary': {
                    "world_title": None,
                    "world_agents": None,
                    "world_masters": None,
                    "world_agents_count": None,
                    "world_masters_count": None,
                    "total_agents": None,
                    "agent_badges_count": None,
                    "agent_badges": None,
                    "streams_count": None
                },
                "world_roles_fsm": None,  # Dict of FSMs for world roles
                "hidden": None
            },
            'cv': cv
        }

    # Checking the presence of basic static profile info
    for k in self._profile_data['static'].keys():
        if (k not in static and k != "certified" and
                k != "allowed_node_ids" and k != "world_masters_node_ids" and k != "inspector_node_id"):  # Patch
            raise ValueError("Missing required static profile info: " + str(k))

    # Filling static profile info (there might be more information that the one shown above)
    for k, v in static.items():
        self._profile_data['static'][k] = v

    # Including the provided dynamic info, only considering the expected keys
    # (the provided "dynamic" argument will contain all or just a sub-portion of the expected keys)
    for k, v in dynamic.items():
        if k == 'connections' and v is not None and isinstance(v, dict):
            for kk, vv in v.items():
                if (kk in self._profile_data['dynamic']['connections'] and
                        self._profile_data['dynamic']['connections'][kk] is None):
                    self._profile_data['dynamic']['connections'][kk] = vv
        elif k == 'world_summary' and v is not None and isinstance(v, dict):
            for kk, vv in v.items():
                if (kk in self._profile_data['dynamic']['world_summary'] and
                        self._profile_data['dynamic']['world_summary'][kk] is None):
                    self._profile_data['dynamic']['world_summary'][kk] = vv
        elif k in self._profile_data['dynamic'] and self._profile_data['dynamic'][k] is None:
            self._profile_data['dynamic'][k] = v
        elif k.startswith('tmp_'):
            self._profile_data['dynamic'][k] = v

    # Internally required attributes
    self._profile_last_updated = None  # Will be set by calling _fill_missing_specs or check_and_update_specs
    self._geolocation_cache = {}  # Will be needed to avoid too many IP-related lookups

    # Filling the missing information (machine-level information, specs) that can be automatically extracted
    self._fill_missing_specs()

    # Flag
    self._connections_updated = False
Methods:
from_dict classmethod
from_dict(combined_data: dict) -> NodeProfile

Factory method to create a NodeProfile instance from a dictionary containing combined profile data (static, specs, and CV list of dicts).

Parameters:

Name Type Description Default
combined_data dict

A dictionary representing the node profile, typically loaded from JSON or received over the network. Expected to contain 'node_id', 'cv' (list of dicts), 'node_specification' (dict), 'peer_id', 'peer_addresses' and other profile keys.

required

Returns:

Name Type Description
NodeProfile NodeProfile

A new instance of NodeProfile populated from the dictionary.

Raises:

Type Description
ValueError

If 'node_id' is missing in the input dictionary.

TypeError

If the 'cv' data is present but not a list.

Source code in unaiverse/networking/node/profile.py
@classmethod
def from_dict(cls, combined_data: dict) -> 'NodeProfile':
    """Factory method to create a NodeProfile instance from a dictionary
    containing combined profile data (static, specs, and CV list of dicts).

    Args:
        combined_data (dict): A dictionary representing the node profile,
                              typically loaded from JSON or received over the network.
                              Expected to contain 'node_id', 'cv' (list of dicts),
                              'node_specification' (dict), 'peer_id', 'peer_addresses'
                              and other profile keys.

    Returns:
        NodeProfile: A new instance of NodeProfile populated from the dictionary.

    Raises:
        ValueError: If 'node_id' is missing in the input dictionary.
        TypeError: If the 'cv' data is present but not a list.
    """

    # Ensure essential 'node_id' is present
    node_id = combined_data.get('static').get('node_id')
    if not node_id:
        raise ValueError("Input dictionary must contain a 'node_id'.")

    profile_instance = cls(
        static=combined_data['static'],
        dynamic=combined_data['dynamic'],
        cv=combined_data['cv']
    )

    return profile_instance
check_and_update_specs
check_and_update_specs(update_only: bool = True) -> bool

Checks current specs against saved specs. Updates profile data.

Source code in unaiverse/networking/node/profile.py
def check_and_update_specs(self, update_only: bool = True) -> bool:
    """Checks current specs against saved specs. Updates profile data."""

    current_specs = self._get_current_specs()
    specs_changed = False

    if update_only:
        self._profile_data['dynamic'] |= current_specs
    else:
        saved_specs = self._profile_data['dynamic'].copy()
        change_details = []

        if saved_specs is None:

            # No previous specification exists, capture the current one
            self._profile_data['dynamic'] |= current_specs
            specs_changed = True
            change_details.append("Initial specification captured")

        else:

            # Compare current specs with saved specs (ignore timestamp for comparison)
            keys_to_compare = current_specs.keys()

            for key in keys_to_compare:
                if key == 'timestamp':
                    continue

                saved_value = saved_specs.get(key)
                current_value = current_specs.get(key)

                # Handle float comparison with tolerance
                if isinstance(saved_value, float) and isinstance(current_value, float):
                    if abs(current_value - saved_value) > 1e-6:  # Tolerance for float changes
                        change_details.append(f"{key}: from {saved_value:.2f} to {current_value:.2f}")
                        specs_changed = True

                elif saved_value != current_value:
                    change_details.append(f"{key}: from {saved_value} to {current_value}")
                    specs_changed = True

            # Comparing total resources (OS, CPU, total RAM/Disk) is more typical for 'specification' changes.
            if specs_changed:

                # Update the specification in the profile data with the new current specs
                self._profile_data['dynamic'] |= current_specs
                change_summary = ", ".join(change_details)
                print(f"Specs changed for '{self._profile_data['static']['node_id']}': {change_summary}")

    self._profile_last_updated = datetime.datetime.now(timezone.utc)  # Mark profile as checked/updated

    return specs_changed