Source code for firebird.uuid._model

# SPDX-FileCopyrightText: 2022-present The Firebird Projects <www.firebirdsql.org>
#
# SPDX-License-Identifier: MIT
#
# PROGRAM/MODULE: firebird-uuid
# FILE:           firebird/uuid/_model.py
# DESCRIPTION:    Model for Firebird OID registry
# CREATED:        11.11.2022
#
# The contents of this file are subject to the MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Copyright (c) 2022 Firebird Project (www.firebirdsql.org)
# All Rights Reserved.
#
# Contributor(s): Pavel Císař (original code)
#                 ______________________________________

"""Model for Firebird OID registry.

"""

from __future__ import annotations
from typing import List, Dict, Optional, Type, Tuple
from enum import Enum
from weakref import proxy
import uuid
from firebird.base.types import Distinct, Error

#: IANA name before Firebird namespace. Does not contain trailing dot.
IANA_ROOT_NAME = 'iso.org.dod.internet.private.enterprise'

KEY_ATTRS = ('oid', 'name', 'number', 'description', 'contact', 'email', 'site', 'node_type', 'node_spec')

[docs] class NodeType(Enum): ROOT = 'root' LEAF = 'leaf' PRIVATE = 'private' NODE = 'node'
[docs] class Node(Distinct): """OID node. Arguments: parent: Parent node (`None` for root node) oid: Node OID. When `None`, OID is constructed from parent OID and `number` parameter. number: Node order number in parent. Part of node OID for child nodes. name: Node name. description: Node description. contact: Contact person for this node. email: E-mail of contact person. site: URL to home site of node maintainer parent_spec: URL to YAML specification of parent node. If not specified, it's taken from `parent` node. node_spec: URL to node TAML specification node_type: Node type. If not specified, it's derived from `node_spec`. """ def __init__(self, *, parent: Optional[Node]=None, oid: Optional[str]=None, number: Optional[int]=None, name: Optional[str]=None, description: Optional[str]=None, contact: Optional[str]=None, email: Optional[str]=None, site: Optional[str]=None, parent_spec: Optional[str]=None, node_spec: Optional[str]=None, node_type: Optional[str]=None): #: Parent node (or None for ROOT) self.parent: Optional[Node] = None if parent is None else proxy(parent) self.__oid: Optional[str] = oid #: OID self.oid: Optional[str] = oid if number is not None and parent is not None: self.oid = parent.oid + '.' + str(number) #: UUID self.uid: uuid.UUID = uuid.uuid5(uuid.NAMESPACE_OID, self.oid) #: Node number (OID part after parent node OID) self.number: Optional[int] = number #: Node name self.name: Optional[str] = name #: Node description self.description: Optional[str] = description #: Name of node administrator self.contact: Optional[str] = contact #: E-mail to node administrator self.email: Optional[str] = email #: URL to node administrator home self.site: Optional[str] = site #: URL to parent node specification self.parent_spec: Optional[str] = parent_spec if parent_spec is None and parent: self.parent_spec = parent.node_spec #: URL to node specification self.node_spec: Optional[str] = node_spec self.__node_type: Optional[str] = node_type #: Node type self.node_type: Optional[NodeType] = None if node_type: self.node_type = NodeType._value2member_map_.get(node_type.lower()) elif node_type is None and node_spec: self.node_type = NodeType._value2member_map_.get(node_spec.lower(), NodeType.NODE) if self.node_type in (NodeType.LEAF, NodeType.PRIVATE): self.node_spec = None #: Child nodes self.children: List[Node] = []
[docs] def get_key(self) -> uuid.UUID: "Returns node key -> UID" return self.uid
[docs] def set_parent(self, parent: Node) -> None: """Set new parent node. Important: This method does NOT change the parent's children list! """ self.parent = None if parent is None else proxy(parent) if self.number is not None and parent is not None: self.oid = parent.oid + '.' + str(self.number) if parent is not None: self.parent_spec = parent.node_spec
[docs] def as_toml_dict(self) -> Dict: """Returns dictionary with instance data suitable for storage in TOML format (values that are not of basic type are converted to string). """ return {'parent': str(self.parent.uid) if self.parent else None, 'oid': self.__oid, 'number': self.number, 'name': self.name, 'description': self.description, 'contact': self.contact, 'email': self.email, 'site': self.site, 'node_spec': (self.node_type.value if self.node_type in (NodeType.LEAF, NodeType.PRIVATE) else self.node_spec), 'node_type': self.__node_type, }
[docs] @classmethod def from_spec(cls: Type[Node], spec_url: str, data: Dict, parent: Optional[Node]=None) -> Node: """Returns new node from specification. Arguments: spec_url: Source URL for specification. data: Parsed and validated data from OID node specification document parent: Parent node. """ node: Node = cls(parent=parent, node_spec=spec_url, **data['node']) for child in data['children']: node.children.append(cls(parent=node, **child)) return node
@property def full_name(self) -> str: "Full node name (from root node)" return self.name if self.parent is None else self.parent.full_name + '.' + self.name
def validate_parent_child_equality(parent: Node, child: Node) -> None: """Check that `KEY_ATTRS` in parent and child node are equal. Used when child node is merged (replaced) with equvalent node from linked specification. Raises: ValueError: If values of any checked attribute differ. """ for attr in KEY_ATTRS: if getattr(parent, attr) != getattr(child, attr): raise ValueError(f"Parent and node spec. differ in attribute '{attr}'") def build_tree(nodes: List[Node]) -> Node: """Returns root node of node tree assmebled from list of nodes. Arguments: nodes: List of nodes. Must contain ROOT node. Raises: Error: If list of nodes does not contain ROOT node. """ def traverse(_root: Node, _node: Optional[Node]=None) -> None: if _node is None: _node = _root for i, child in enumerate(_node.children): if child.node_type is NodeType.NODE: sub_node = node_map[child.node_spec] sub_node.number = child.number validate_parent_child_equality(child, sub_node) sub_node.set_parent(_node) _node.children[i] = sub_node traverse(_root, sub_node) node_map: Dict[str, Node] = {} root: Node = None for node in nodes: node_map[node.node_spec] = node if node.node_type is NodeType.ROOT: root = node if root is None: raise Error("ROOT node not found") # traverse(root) return root