PyDCC is a software library for reading and automated processing of digital calibration certificates (DCCs).
# PyDCC
#
# Init file
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
from .dcc import DCC
from .dcc import DCCStatusType
from .dcc import DCCStatusReport
dcc/__init__.py
siemens/pydcc
# PyDCC
#
# Python module for processing of digital calibration certificates (DCC)
# according to https://www.ptb.de/dcc/
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#

import datetime
import xml.etree.ElementTree as ET
import zlib
import binascii
from collections import defaultdict
import requests
from dataclasses import dataclass

from signxml import InvalidCertificate, InvalidSignature, InvalidInput
from certvalidator import CertificateValidator, errors, ValidationContext
from signxml.xades import XAdESVerifier
from asn1crypto import pem
from cryptography import x509
from typing import Optional

from .dcc_xml_validator import DCCXMLValidator



class DCCStatusType:
    """Status report classes"""
    IS_LOADED = 0
    VALID_SCHEMA = 1
    IS_SIGNED = 2
    VALID_SIGNATURE = 3

@dataclass
class DCCStatusReport:
    """
    Status report for a specific DCC.
    This status report is a checklist represented by a set of binary values.
    This report is created by the class DCC.
    """

    loading_performed: bool = False
    is_loaded: bool = False

    schema_verification_performed: bool = False
    valid_schema: bool = False

    is_signed: bool = False
    signature_verification_performed: bool = False
    valid_signature: bool = False

    def report(self, status_type: DCCStatusType, success=True):
        """Report test results with this method-"""
        if status_type == DCCStatusType.IS_LOADED:
            self.loading_performed = True
            self.is_loaded = success
        elif status_type == DCCStatusType.VALID_SCHEMA:
            self.schema_verification_performed = True
            self.valid_schema = success
        elif status_type == DCCStatusType.IS_SIGNED:
            self.is_signed = success
        elif status_type == DCCStatusType.VALID_SIGNATURE:
            self.signature_verification_performed = True
            self.valid_signature = success
        else:
            raise TypeError("Unknown DCCStatusType was passed to DCCStatusReport.")


    # ignore_list: Optinal[DCCStatusType] = None
    def get_status_summary(self, ignore_list=[]):
        """Read status report summary."""
        overall_status = True

        if DCCStatusType.VALID_SCHEMA not in ignore_list:
            overall_status = overall_status and self.valid_schema

        if DCCStatusType.VALID_SIGNATURE not in ignore_list:
            overall_status = overall_status and self.valid_signature

        return overall_status


class DCCTrustStore:
    def __init__(self):
        self.trust_roots = []
        self.intermediates = []

    def load_trusted_root_from_file(self, file_name):
        cert = None
        with open(file_name, 'rb') as f:
            cert = f.read()
            if pem.detect(cert):
                type_name, headers, der_bytes = pem.unarmor(cert)
                cert = der_bytes
        self.trust_roots.append(cert)

    def load_intermediate_from_file(self, file_name):
        """ update trust intermediate list """
        cert = None
        with open(file_name, 'rb') as f:
            cert = f.read()
            if pem.detect(cert):
                type_name, headers, der_bytes = pem.unarmor(cert)
                cert = der_bytes
        self.intermediates.append(cert)


class DCC:
    """
    Python module for processing of digital calibration certificates (DCC)
    """

    def __init__(
        self,
        xml_file_name: Optional[str] = None,
        byte_array: Optional[bytearray] = None,
        compressed_dcc: Optional[bytearray] = None,
        url: Optional[str] = None,
        signature_verification: bool = True,
        trust_store: Optional[DCCTrustStore] = None
    ):
        # Initialize DCC object
        self.status_report = DCCStatusReport()
        self.xml_file_name = xml_file_name
        self.administrative_data: Optional[ET.Element] = None
        self.measurement_results: Optional[ET.Element] = None
        self.root: Optional[ET.Element] = None
        self.root_byte: Optional[bytearray] = None
        self.datetime_file_loaded = datetime.datetime.now()
        self.name_space = dict()
        self.UID: str = ""
        self.signature_section: Optional[ET.Element] = None
        self.schema_sources = []
        self.signature_verification = signature_verification
        self.trust_store = trust_store
        self.xml_validator = DCCXMLValidator()

        # Set default DCC namespaces
        self.__add_namespace('dcc', 'https://ptb.de/dcc')
        self.__add_namespace('si', 'https://ptb.de/si')
        self.__add_namespace('ds', 'http://www.w3.org/2000/09/xmldsig#')
        self.__add_namespace('xades', 'http://uri.etsi.org/01903/v1.3.2#')

        if xml_file_name is not None:
            self.__load_dcc_from_xml_file()
        elif byte_array is not None:
            self.__load_dcc_from_byte_array(byte_array)
        elif compressed_dcc is not None:
            self.__load_compressed_dcc(compressed_dcc)
        elif url is not None:
            self.__load_dcc_from_public_server(url)
        else:
            raise Exception('PyDCC: DCC object created without giving an XML source.')

        if self.root is not None:
            self.administrative_data = self.root[0]
            # self.administrative_data = root.find("dcc:administrativeData", self.name_space)
            self.measurement_results = self.root[1]
            self.dcc_version = self.root.attrib['schemaVersion']
            # self.valid_xml = self.verify_dcc_xml()
            self.UID = self.uid()
            self.status_report.report(DCCStatusType.IS_LOADED)
            if self.__is_signed():
                self.status_report.report(DCCStatusType.IS_SIGNED)
                if self.signature_verification:
                    self.__verify_signature()

    def __verify_signature(self):
        if len(self.__find_signatures()) > 1:
            raise DCCSignatureError("Counter signatures and parallel signatures are currently not supported.")

        # Step 0: Check if there is a trust store with root and intermediate CA certificates
        if self.trust_store is None:
            self.status_report.report(DCCStatusType.VALID_SIGNATURE, False)
            raise DCCSignatureError("Could not validate certificate path because of missing trust store.")
        if self.trust_store.trust_roots is None:
            self.status_report.report(DCCStatusType.VALID_SIGNATURE, False)
            raise DCCSignatureError("Could not validate certificate path because of missing root CA certificate.")
        if self.trust_store.intermediates is None:
            self.status_report.report(DCCStatusType.VALID_SIGNATURE, False)
            raise DCCSignatureError("Could not validate certificate path because of missing intermediate CA certificate.")

        # Step 1: Validate certificate that was used to sign the DCC
        signing_cert = self.root.find(".//ds:X509Certificate", self.name_space).text
        signing_cert_pem = '-----BEGIN CERTIFICATE-----\n' + signing_cert + '\n-----END CERTIFICATE-----'
        signing_time = self.root.find(".//xades:SigningTime", self.name_space)

        if signing_time is not None:
            validation_time = datetime.datetime.fromisoformat(signing_time.text.replace('Z', '+00:00'))
        else:
            validation_time = datetime.datetime.now()

        # Get revocation information, if there are any
        crls = None
        ocsps = None
        revocation_mode = "soft-fail"
        crl_infos = self.root.findall(".//xades:EncapsulatedCRLValue", self.name_space)
        if len(crl_infos) > 0:
            crls =[]
            revocation_mode = "require"
            for crl_info in crl_infos:
                crls.append(binascii.a2b_base64(str.encode(crl_info.text)))

        ocsp_infos = self.root.findall(".//xades:EncapsulatedOCSPValue", self.name_space)
        if len(ocsp_infos) > 0:
            ocsps = []
            revocation_mode = "require"
            for ocsp_info in ocsp_infos:
                ocsps.append(binascii.a2b_base64(str.encode(ocsp_info.text)))

        context = ValidationContext(trust_roots=self.trust_store.trust_roots, moment=validation_time, crls=crls, ocsps=ocsps, revocation_mode=revocation_mode)
        validator = CertificateValidator(
            pem.armor('CERTIFICATE', binascii.a2b_base64(str.encode(signing_cert))),
            validation_context=context, intermediate_certs=self.trust_store.intermediates)
        try:
            validator.validate_usage(set())
        except errors.RevokedError as e:
            self.status_report.report(DCCStatusType.VALID_SIGNATURE, False)
            raise DCCSignatureError("Revocation error.  Original exception message was: " + str(e)) from None  # PEP 409 supress original exception
        except errors.PathValidationError as e:
            # The certificate could not be validated
            self.status_report.report(DCCStatusType.VALID_SIGNATURE, False)
            raise DCCSignatureError("Could not validate certificate path.  Original exception message was: " + str(e)) from None

        # Step 2: Validate DCC signature

        # find the number of references to verify
        num_refs = len(self.root.findall(".//ds:Reference", self.name_space))
        if num_refs < 2:
            self.status_report.report(DCCStatusType.VALID_SIGNATURE, False)
            raise DCCSignatureError(
                "Expected signed DCC as XadES - but < 2 references are found in the signature (violating ETSI EN 319 "
                "132-1 -> see Table 2)")

        # try to verify signature using signer certificate which is parsed from the XML signature
        data_to_verify = self.root_byte
        try:
            data = XAdESVerifier().verify(data_to_verify, x509_cert=signing_cert_pem,
                                          expect_references=num_refs)  # expect references due to XADES signature format
        except InvalidCertificate as e:
            self.status_report.report(DCCStatusType.VALID_SIGNATURE, False)
            raise DCCSignatureError("Signing certificate invalid. Original exception message was: " + str(e)) from None
        except InvalidSignature as e:
            self.status_report.report(DCCStatusType.VALID_SIGNATURE, False)
            raise DCCSignatureError("Signature is invalid. Original exception message was: " + str(e)) from None
        except InvalidInput as e:
            self.status_report.report(DCCStatusType.VALID_SIGNATURE, False)
            raise DCCSignatureError("Provided XML does not include enveloped signature. Original exception message was: " + str(e)) from None

        # Store signed data from DCC (without enveloped signature) in root element
        found_dcc_elem = False
        for data_elem in data:
            if data_elem.signed_xml.tag == '{https://ptb.de/dcc}digitalCalibrationCertificate':
                found_dcc_elem = True
                # Store signature from verified xml in signature element
                self.root = data_elem.signed_xml
                # Store signature from verified xml in signature element
                self.signature_section = data_elem.signature_xml
                break

        if not found_dcc_elem:
            raise DCCSignatureError("DCC element was not found in VerifyResults of signature validation")

        # Signature was valid
        self.status_report.report(DCCStatusType.VALID_SIGNATURE, True)

    def get_signer_certificate(self):
        if self.signature_section is None:
            raise DCCSignatureError('No signature section available for this DCC object. Either the DCC does not have '
                                    'a signature or the signature was not verified or is invalid.')
        signing_cert = self.signature_section.find(".//ds:X509Certificate", self.name_space).text
        if signing_cert is None:
            raise DCCSignatureError('No signer certificate available in signature section.')
        signing_cert_pem = '-----BEGIN CERTIFICATE-----\n' + signing_cert + '\n-----END CERTIFICATE-----'
        return x509.load_pem_x509_certificate(str.encode(signing_cert_pem))

    def get_signing_time(self):
        if self.signature_section is None:
            raise DCCSignatureError('No signature section available for this DCC object. Either the DCC does not have '
                                    'a signature or the signature was not verified or is invalid.')
        signing_time = self.signature_section.find(".//xades:SigningTime", self.name_space)
        if signing_time is None:
            raise DCCSignatureError('No signing time available in signature section')
        return datetime.datetime.fromisoformat(signing_time.text.replace('Z', '+00:00'))

    def __load_dcc_from_xml_file(self):
        # Load DCC from file
        with open(self.xml_file_name, "rb") as file:
            byte_array = file.read()
            self.__load_dcc_from_byte_array(byte_array)

    def __load_dcc_from_byte_array(self, byte_array):
        # Load DCC from file
        self.dcc_xml_raw_data = byte_array
        self.root_byte = byte_array
        self.root = ET.fromstring(byte_array)

    def __load_dcc_from_public_server(self, server_url, server_port=8085, dcc_id=None, item_id=None):
        success = False
        # Load DCC from server (PROTOTYPE)
        query_address = server_url  # + dcc_id # URL encode, special chars
        response = requests.get(query_address)
        if response.status_code == 200:
            byte_array = response.content
            self.__load_dcc_from_byte_array(byte_array)
            success = True
        return success

    def __load_compressed_dcc(self, byte_array):
        # Load compressed DCC
        self.dcc_xml_raw_data = zlib.decompress(byte_array)
        self.__load_dcc_from_byte_array(self.dcc_xml_raw_data)

    def is_loaded(self):
        # Check if DCC was loaded successfully
        return self.status_report.is_loaded

    def __add_namespace(self, name_space_label, name_space_url):
        # Add namespace
        self.name_space[name_space_label] = name_space_url

    def verify_dcc_xml(self, online: bool):
        # Verify DCC file
        valid_xml = self.xml_validator.dcc_is_valid_against_schema(self.root, online=online,
                                                                   dcc_version=self.dcc_version)
        self.status_report.report(DCCStatusType.VALID_SCHEMA, valid_xml)
        return valid_xml

    def __find_signatures(self):
        return self.root.findall("ds:Signature", self.name_space)

    def __is_signed(self):
        # Is the DCC signed?
        if len(self.__find_signatures()) > 0:
            return True

    def calibration_date(self):
        # Return calibration date (endPerformanceDate)
        elem = self.root.find("dcc:administrativeData/dcc:coreData/dcc:endPerformanceDate", self.name_space)
        date_string = elem.text
        daytime_obj = datetime.datetime.strptime(date_string, '%Y-%m-%d')
        return daytime_obj

    def days_since_calibration(self):
        # Return number of days since calibration (endPerformanceDate)
        dt_now = datetime.datetime.now()
        dt_calibration = self.calibration_date()
        diff_obj = dt_now - dt_calibration
        days_since_calibration = diff_obj.days
        return days_since_calibration

    def calibration_laboratory_name(self):
        # Return calibration lab name
        elem = self.root.find("dcc:administrativeData/dcc:calibrationLaboratory/dcc:contact/dcc:name/dcc:content",
                              self.name_space)
        return elem.text

    def uid(self):
        # Return unique ID
        elem = self.root.find("dcc:administrativeData/dcc:coreData/dcc:uniqueIdentifier", self.name_space)
        uid_string = elem.text
        return uid_string

    def mandatory_language(self):
        # Return mandatory Language Code
        elem = self.root.find("dcc:administrativeData/dcc:coreData/dcc:mandatoryLangCodeISO639_1", self.name_space)
        mandatory_lang = elem.text
        return mandatory_lang

    def version(self):
        # Return DCC version
        return self.dcc_version

    def has_previous_report(self):
        # Check for previous report
        # Returns true if available
        self.previous_report = self.root.find("dcc:administrativeData/dcc:coreData/dcc:previousReport", self.name_space)
        previous_report_available = not self.previous_report == None
        return previous_report_available

    def generate_compressed_dcc(self):
        # Convert DCC to C header file for DCC integration on constraint devices

        dcc_xml_raw_data_compressed = zlib.compress(self.dcc_xml_raw_data)
        bytes_uncompressed = len(self.dcc_xml_raw_data)
        bytes_compressed = len(dcc_xml_raw_data_compressed)
        compression_ratio = bytes_compressed / bytes_uncompressed

        # CRC32: crc32(data) & 0xffffffff to generate the same numeric value across all Python versions and platforms.
        dcc_crc32 = zlib.crc32(self.dcc_xml_raw_data) & 0xffffffff

        ret_dict = dict()
        ret_dict['bytes_uncompressed'] = bytes_uncompressed
        ret_dict['bytes_compressed'] = bytes_compressed
        ret_dict['compression_ratio'] = compression_ratio
        ret_dict['dcc_xml_raw_data_compressed'] = dcc_xml_raw_data_compressed
        ret_dict['crc32'] = dcc_crc32

        compressed_dcc_data_in_c = "const uint8_t compressed_dcc[%d] = {" % bytes_compressed
        k = 0
        for byte in dcc_xml_raw_data_compressed:
            k += 1
            hex_str = format(byte, '02x')
            hex_str_with_preamble = "0x" + hex_str
            compressed_dcc_data_in_c += hex_str_with_preamble
            if not k == bytes_compressed:
                compressed_dcc_data_in_c += ", "
            if k % 10 == 0:
                compressed_dcc_data_in_c += "\n"
        compressed_dcc_data_in_c += "};"

        compressed_dcc_data_in_c += "\n"
        compressed_dcc_data_in_c += "const uint32_t compressed_dcc_crc32  = 0x%x; // CRC32 according to python module zlib\n" % dcc_crc32
        compressed_dcc_data_in_c += "const uint32_t compressed_dcc_size   = %u; // size in bytes\n" % bytes_compressed
        compressed_dcc_data_in_c += "const uint32_t uncompressed_dcc_size = %u; // size in bytes\n" % bytes_uncompressed

        ret_dict['compressed_dcc_data_in_c'] = compressed_dcc_data_in_c
        return ret_dict

    def __read_name(self, node, name, lang):
        local_name = node.find('dcc:name/dcc:content[@lang=' + "\'" + lang + "\'" + ']', self.name_space)
        if local_name is not None:
            name = name + ' ' + local_name.text
        else:
            local_name = node.find('dcc:name/dcc:content', self.name_space)
            if local_name is not None:
                name = name + ' ' + local_name.text
        return name

    def __read_path_realted_info(self, node, attr):
        attr = attr + "dcc:" + str(str(node.tag.rpartition('}')[2]))
        if "refType" in node.attrib.keys():
            attr = attr + " [ @ refType =" + "\'" + str(node.attrib['refType']) + "\'" + "]"
        if "refId" in node.attrib.keys():
            attr = attr + " [ @ refId =" + "\'" + str(node.attrib['refId']) + "\'" + "]"
        if "id" in node.attrib.keys():
            attr = attr + " [ @ id =" + "\'" + str(node.attrib['id']) + "\'" + "]"

        return attr

    def __find_quantities_in_lists(self, node, quant, name, lang, xpath):
        name = self.__read_name(node, name, lang)
        xpath = self.__read_path_realted_info(node, xpath)

        if node.tag == '{https://ptb.de/dcc}quantity':
            quant.append([node, name, xpath])
        elif node.tag == '{https://ptb.de/dcc}list':
            xpath = xpath + " //"
            for next_node in node:
                self.__find_quantities_in_lists(next_node, quant, name, lang, xpath)

    def get_calibration_results(self, type, lang=''):
        quantities = []
        res = []
        result_nodes = self.root.findall('dcc:measurementResults/dcc:measurementResult/dcc:results/dcc:result',
                                         self.name_space)
        for result in result_nodes:
            xpath = ".//"
            xpath = self.__read_path_realted_info(result, xpath)
            xpath = xpath + " //"

            data_node = result.find('dcc:data', self.name_space)
            name = ''
            name = self.__read_name(result, name, lang)

            for nodes in data_node:
                self.__find_quantities_in_lists(nodes, quantities, name, lang, xpath)

        for quant in quantities:
            si_node = quant[0].find('{https://ptb.de/si}*', self.name_space)
            if si_node is not None:
                if type == 'xpath':
                    local_res = [quant[2], self.__etree_to_dict(si_node)]
                else:
                    local_res = [quant[1], self.__etree_to_dict(si_node)]
                res.append(local_res)
        return res

    def __etree_to_dict(self, t):
        """
        This method was adapted from a post at Stackoverflow
        URL: https://stackoverflow.com/a/10077069
        This method is licensed under a Creative Commons Attribution ShareAlike 3.0 (CC-BY-SA 3.0) License
        URL: http://creativecommons.org/licenses/by/3.0/deed.en_US
        """
        # method to recursively traverse the xml tree from a specified point and to return the elemnts in dictionary form
        tkey = t.tag.rpartition('}')[2]
        tree_dict = {tkey: {} if t.attrib else None}
        children = list(t)
        if children:
            subdict = defaultdict(list)
            for dc in map(self.__etree_to_dict, children):
                for key, val in dc.items():
                    subdict[key].append(val)
            tree_dict = {tkey: {k: v[0] if len(v) == 1 else v
                        for k, v in subdict.items()}}
        if t.attrib:
            tree_dict[tkey].update(('@' + k, v)
                           for k, v in t.attrib.items())
        if t.text:
            text = t.text.strip()
            if children or t.attrib:
                if text:
                    tree_dict[tkey]['#text'] = text
            else:
                tree_dict[tkey] = text
        return tree_dict

    def item_id(self):
        # Retrieve list of items in DCC and return as a dictionary with identifier type as key
        id_list = self.root.find("dcc:administrativeData/dcc:items/dcc:item/dcc:identifications", self.name_space)
        return self.__etree_to_dict(id_list)

    def item_id_new(self):
        # Retrieve list of items in DCC and return as a dictionary with identifier type as key
        id_list = self.root.find("dcc:administrativeData/dcc:items/dcc:item/dcc:identifications", self.name_space)
        item_id_dict = {'identifications': []}
        ids = list(id_list)
        for id in ids:
            item_id_dict['identifications'].append(self.etree_to_dict(id))
        return item_id_dict

    def get_item_id_by_name(self, searched_name, searched_language = None, searched_issuer = None):
        id_list = self.item_id()['identifications']['identification']
        for id in id_list:
            names = id['name']['content']
            issuer = id['issuer']
            if searched_issuer is not None:
                if not issuer == searched_issuer:
                    continue
            for name in names:
                language = name['@lang']
                if searched_language is not None:
                    if not language == searched_language:
                        continue
                name_text =  name['#text']
                if name_text == searched_name:
                    value = id['value']
                    return value
        return None


class DCCSignatureError(Exception):
    """ this exception is raised if any problem with the validation of the DCC signature occurs"""


dcc/dcc.py
siemens/pydcc
# PyDCC
#
# Python module for processing of digital calibration certificates (DCC)
# according to https://www.ptb.de/dcc/
#
# Copyright (c) Siemens AG, 2022
#
# Authors:
#     Kai Mienert, PTB
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#

"""
This module can be used to validate a Digital Calibration Certificate (DCC). The DCCXMLValidator class is used to
perform the validation. The Physikalisch-Technische Bundesanstalt provides a Json document. This document lists
all current DCC schemas. The class DCCXMLValidator downloads all schemas listed in the json file. For validation
the function dcc_is_valid_against_schema can be used. The user specifies whether the previously downloaded schemas
are used or the schemaLocations in the DCC-xml file are used.

The DCC schemas and all other dependencies are stored in a workspace. This is usually located in a specified folder.
However, the user can define the workspace himself. For licensing reasons, the schemas cannot be stored in the
project/module directory.
"""

import requests
import json
import os
from typing import List, Optional
import platform
import xmlschema
import xml.etree.ElementTree as ET
from pydantic import BaseModel, ValidationError
from datetime import datetime

# "url to load a json documents that hold information about the DCC versions"
url_dcc_schema_releases = "https://www.ptb.de/dcc/releases.json"


class DccXmlValidator(Exception):
    """General exception."""


class DccSchemaNotAvailableLocal(DccXmlValidator):
    """No locally available schema was found for the DCC to be validated."""


def get_standard_workspace_path() -> os.path:
    # Get the system the programm is running on to decide what folder should be used

    system = platform.system()
    if system == "Windows":
        appdata_path = os.getenv('APPDATA')
    elif system == "Linux":
        appdata_path = os.path.expanduser('~')
    elif system == "Darwin":
        appdata_path = os.path.expanduser('~/Library/Application Support')
    else:
        return None

    folder_path = os.path.join(appdata_path, 'pydcc')
    if not os.path.exists(folder_path):
        os.makedirs(folder_path)

    return folder_path


def get_imports_from_xml(xml_string: str):
    root = ET.fromstring(xml_string)

    # Find all elements with the tag "import"
    import_elements = root.findall(".//{*}import")
    dependencys: List[str] = []

    # Iterate over each import element
    for element in import_elements:
        if "schemaLocation" in element.attrib:
            dependencys.append(element.attrib["schemaLocation"])

    return dependencys


def get_target_namespace_from_xml(xml_string: str):
    root = ET.fromstring(xml_string)
    if "targetNamespace" in root.attrib:
        return root.attrib.get("targetNamespace")
    return ""


def get_schema_version(xml_string: str):
    root = ET.fromstring(xml_string)
    if "schemaVersion" in root.attrib:
        return root.attrib.get("schemaVersion")
    if "version" in root.attrib:
        return root.attrib.get("version")
    return ""


class Schema(BaseModel):
    # Data to describe the schema
    targetNamespace: str = ""
    filename: str = ""
    version_str: str = ""
    location_local_abs: str = ""
    online_schemaLocation: str = ""

    # dependencies
    dependencys: List[str] = []


class DCCRelease(BaseModel):
    """
    used subelement to parse json file from  https://www.ptb.de/dcc/releases.json
    """
    version: str = ""
    url: str = ""
    type: str = ""


class DCCOnlineReleases(BaseModel):
    """
    parse json file from  https://www.ptb.de/dcc/releases.json
    """
    description: str = ""
    releases: List[DCCRelease] = DCCRelease()


class Data(BaseModel):
    """
    store information about downloaded schemas
    """
    online_available_releases: DCCOnlineReleases = DCCOnlineReleases()
    local_available_schemas: List[Schema] = []
    last_update: datetime = datetime.now()


WORKSPACE_STATUS_OK = 0
WORKSPACE_PATH_DOES_NOT_EXIST = 1
WORKSPACE_NO_README = 2
WORKSPACE_STATUS_NO_DATA_FILE = 3
WORKSPACE_STATUS_MISSING_SCHEMA_FILES = 4
WORKSPACE_STATUS_JSON_FILE_CORRUPTED = 5


class DCCXMLValidator:

    def __init__(self, path_to_workspace: Optional[str] = None, workspace_init: bool = True):
        """
        DCCXMLValidator can be used to validate the DCC against the schema. Online validation is supported,
        where the dcc schema is downloaded from the source specified in the dcc. It is also possible to validate
        the dcc against locally stored schemas. These are stored in a workspace.

        :param path_to_workspace: set path to workspace, if not set then a default workspace path is used.
            Path depends on os.
        :type path_to_workspace: string
        :param workspace_init:  Init the workspace if it has not yet been
            initiated or is not valid.
        :type workspace_init: bool
        """

        #  set workspace where schemas will be stored locally on the machine. Used to perform offline schema validation
        if path_to_workspace is None:
            self.path_workspace = get_standard_workspace_path()
        else:
            self.path_workspace = path_to_workspace

        self.path_data_file = os.path.join(self.path_workspace, "data.json")

        # check if workspace is valid
        self.workspace_status = self.__get_workspace_status()

        self.data: Optional[Data] = None

        # if workspace is not valid try to initiate it
        if workspace_init and not self.workspace_status == WORKSPACE_STATUS_OK:
            self.__init_workspace()
            self.workspace_status = self.__get_workspace_status()

        # if workspace is valid load data.json. These file contains alls local available schemas
        if self.workspace_status == WORKSPACE_STATUS_OK:
            with open(self.path_data_file, mode="r", encoding="utf-8") as file:
                self.data = Data(**json.load(file))

        self.previous_used_schmas = {}

    def __init_workspace(self):
        # Create a Readme.txt file inside the Folder with some information about the usage.

        content = "This directory was created by the pyDCC software.\nIt is used to store downloaded xml schemas." \
                  "\nThese are used for the validation of the DCCs." + "\n\n" + "Created on: " + \
                  str(datetime.now())
        path_readme = os.path.join(self.path_workspace, 'Readme.txt')
        if not os.path.exists(path_readme):
            with open(path_readme, mode="w") as file:
                file.write(content)

        self.__download_list_with_available_releases()

        self.__download_schemas_referenced_by_list()

        self.__download_dependencys()

        if self.data is not None:
            with open(self.path_data_file, mode="w") as file:
                file.write(self.data.json())

    def __get_workspace_status(self) -> int:
        """
        Check the workspace.

        1. Test if the path to the workspace folder exists and user have access
        2. Test if there is a Readme.txt file inside the workspace
        3. Test if there is a data.json file insode the workspace
        4. Try to parse the data.json file
        5. Test if all loacal xsd files referenced ba data.json exists

        :return: Status of workspace (WORKSPACE_STATUS_OK,WORKSPACE_PATH_DOES_NOT_EXIST,WORKSPACE_NO_README
        ,WORKSPACE_STATUS_NO_DATA_FILE,WORKSPACE_STATUS_MISSING_SCHEMA_FILES,WORKSPACE_STATUS_JSON_FILE_CORRUPTED)
        :rtype: int
        """

        # 1. Test if the path to the workspace folder exists
        if not os.path.exists(self.path_workspace):
            return WORKSPACE_PATH_DOES_NOT_EXIST

        if not os.access(self.path_workspace, os.W_OK | os.R_OK):
            raise PermissionError("Check the choosen workspace directory " + "\"" + str(
                self.path_workspace) + "\"" + " but the current user has no access rights")

        # 2. Test if there is a Readme.txt file inside the workspace
        if not os.path.isfile(os.path.join(self.path_workspace, "Readme.txt")):
            return WORKSPACE_NO_README

        # 3. Test if there is a data.json file insode the workspace
        if not os.path.isfile(os.path.join(self.path_workspace, "data.json")):
            return WORKSPACE_STATUS_NO_DATA_FILE

        # 4. Try to parse the data.json file
        with open(self.path_data_file, mode="r", encoding="utf-8") as file:
            try:
                self.data = Data(**json.load(file))
            except ValidationError:
                return WORKSPACE_STATUS_JSON_FILE_CORRUPTED

            # return "can not parse data.json file"

        # 5. Test if all local xsd files referenced ba data.json exists
        for local_schema in self.data.local_available_schemas:
            if not os.path.isfile(local_schema.location_local_abs):
                return WORKSPACE_STATUS_MISSING_SCHEMA_FILES

        return WORKSPACE_STATUS_OK

    def dcc_is_valid_against_schema(self, dcc_etree: ET, dcc_version: str,
                                    online: bool) -> bool:
        """
        Validate DCC against the dcc schema

        :param dcc_etree: XML etree object build from dcc.xml
        :param dcc_version: version of teh dcc in case of online = False
        :param online: download referenced dcc schemas using location hints or use local stored xsd files
        :return: true if dcc is valid
        """

        if online:
            return xmlschema.is_valid(xml_document=dcc_etree)
        else:

            if dcc_version in self.previous_used_schmas:
                schema = self.previous_used_schmas[dcc_version]
                return xmlschema.is_valid(xml_document=dcc_etree, schema=schema, use_location_hints=False)

            schema_info: Optional[Schema] = None

            if self.data is not None:
                for local_available_schema in self.data.local_available_schemas:
                    if local_available_schema.version_str == dcc_version:
                        schema_info = local_available_schema
                        break
            else:
                raise DccSchemaNotAvailableLocal

            if schema_info is None:
                raise DccSchemaNotAvailableLocal

            with open(schema_info.location_local_abs, 'r') as file:
                schema_file = file.read()

            local_schema_locations = {"https://ptb.de/dcc": schema_info.location_local_abs}

            for dependency in schema_info.dependencys:
                for local_available_schema in self.data.local_available_schemas:
                    if local_available_schema.online_schemaLocation == dependency:
                        local_schema_locations[
                            local_available_schema.targetNamespace] = local_available_schema.location_local_abs

            schema = xmlschema.XMLSchema(schema_file, allow="sandbox",
                                         base_url=self.path_workspace,
                                         locations=local_schema_locations)

            self.previous_used_schmas[dcc_version] = schema

            return xmlschema.is_valid(xml_document=dcc_etree, schema=schema, use_location_hints=False)

    def __download_list_with_available_releases(self):

        download = requests.get(url_dcc_schema_releases, allow_redirects=True)
        self.data = Data()
        self.data.online_available_releases = DCCOnlineReleases(**(download.json()))

    def __download_schemas_referenced_by_list(self):

        for release in self.data.online_available_releases.releases:
            if release.type != "stable":
                continue

            download = requests.get(release.url, allow_redirects=True)

            # save downloaded xsd schema content to file
            filename = "dcc_" + release.version.replace(".", "_") + ".xsd"
            with open(os.path.join(self.path_workspace, filename), mode="wb") as file:
                file.write(download.content)

            schema = Schema(targetNamespace=get_target_namespace_from_xml((download.content.decode("utf-8"))),
                            filename=filename,
                            version_str=release.version,
                            location_local_abs=str(os.path.join(self.path_workspace, filename)),
                            online_schemaLocation=release.url,
                            dependencys=get_imports_from_xml((download.content.decode("utf-8"))))

            self.data.local_available_schemas.append(schema)

    def __download_dependencys(self):

        # The schemas are uniquely identified by their schemaLocation. However, the generated filename cannot be
        # unique if the schemaLocation changes but the schema does not. For this reason an uid was introduced.
        # Another solution would be the use of a hash value.
        uid = 0

        for local_dcc_schema in self.data.local_available_schemas:
            for dependecny in local_dcc_schema.dependencys:
                try:
                    download = requests.get(dependecny, allow_redirects=True)
                except Exception as e:
                    print(e)
                    continue
                # save downloaded xsd schema content to file
                # check for xml content
                if "<?xml" not in download.content.decode("utf-8")[0:10]:
                    continue

                target_namespace = get_target_namespace_from_xml(download.content.decode("utf-8"))
                version = get_schema_version(download.content.decode("utf-8"))

                uid += 1
                filename = target_namespace.split("/")[-1] + "_" + version.replace(".", "__") + str(uid) + ".xsd"
                with open(os.path.join(self.path_workspace, filename), mode="wb") as file:
                    file.write(download.content)

                new_schema = Schema(targetNamespace=target_namespace,
                                    filename=filename,
                                    version_str=version.replace(".", "_"),
                                    location_local_abs=str(os.path.join(self.path_workspace, filename)),
                                    online_schemaLocation=dependecny)

                # INFO: if you have to download the dependencies of the dependencies (and so on)
                # add dependencys=get_imports_from_xml((download.content.decode("utf-8")) and
                # run this loop until now more schemas will be downloaded

                already_added = False

                for schema in self.data.local_available_schemas:
                    if schema.online_schemaLocation == new_schema.online_schemaLocation:
                        already_added = True
                        break

                if not already_added:
                    self.data.local_available_schemas.append(new_schema)

dcc/dcc_xml_validator.py
siemens/pydcc
# PyDCC
#
# Minimal working example
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
import sys
import zlib

sys.path.append("../dcc/")
from dcc import DCC
# (1) Load DCC and create the DCC object (dcco)
#dcco = DCC('../data/dcc/siliziumkugel_2_4_0.xml')
dcco = DCC('../data/dcc/dcc_gp_temperature_typical_v12.xml')
#dcco = DCC('../data/gp/dcc_gp_temperatur_resistance_v12.xml')
#dcco = DCC('../data/gp/dcc_gp_temperature_extensive_v12.xml')
#dcco =DCC('../data/gp/dcc_ngp_temperature_extensive_v12_some_quant_with_same_refType.xml')
#dcco= DCC('../data/dcc/dcc_ngp_temperature_typical_v12_refType2ID.xml')
#dcco= DCC('../data/KMT/Lagerring_2022-07-13.xml')
#dcco = DCC('C:/Users/janzen01/Documents/GEMIMEG/InputKalibrierscheine/DCC3_1_2/VCMM_KMG/VCMM1_param.xml')
#dcco = DCC('C:/Working_D/xml/example/PORTAL_CMM_KJ/Log/VCMM_DCC.xml')
#dcco = DCC('C:/Users/janzen01/Documents/GEMIMEG/InputKalibrierscheine/DCC3_1_2/Luca/Beispiel-DCC_PTB_id.xml')
#dcco =DCC('C:/Users/janzen01/Documents/GEMIMEG/InputKalibrierscheine/DCC3_1_2/DKD_Laenge/Beispiel-DCC_PTB_KJ.xml')
#dcco = DCC('C:/Users/janzen01/Documents/GEMIMEG/InputKalibrierscheine/DCC3_1_2/Luca/Beispiel-DCC_2022_10_18.xml')
#dcco = DCC('C:/Users/janzen01/Documents/GEMIMEG/InputKalibrierscheine/DCC3_1_2/2D-Normal/2D_Normal_2023-01-23.xml')
#dcco = DCC('C:/Users/janzen01/Documents/GEMIMEG/InputKalibrierscheine/DCC3_1_2/Kugel/Kugelnormal_20230102__refType_ID.xml')
#
#print("single result")
#res = dcco.get_calibration_result_by_quantity_id("basic_measurementError")
#print(res)
#dcco = DCC('../data/gp/dcc_gp_temperature_extensive_v12.xml')
#dcco = DCC('../data/gp/dcc_gp_temperature_typical_v12.xml')
#dcco = DCC('../data/dcc/dcc_gp_temperature_typical_v12.xml')

#comp_dcc = dcco.generate_compressed_dcc()
#crc32 = comp_dcc['crc32']
#print (crc32)


#res = dcco.get_calibration_result_by_quantity_id('xz')
#print("function that extracts si-information of a quantity with id: xz")
#print(res)
#dcco = DCC('C:/Users/janzen01/Documents/GEMIMEG/InputKalibrierscheine/DCC3_1_2/2D-Normal/Zweidimensionales_Normal.xml')

print("alle Resultate mit Namen auf deutsch, wenn möglich")
res = dcco.get_calibration_results('name', 'de')
for i in res:
   print(i)

print("alle Resultate mit Namen auf englisch, wenn möglich")
res = dcco.get_calibration_results('name', 'en')
for i in res:
   print(i)

print("alle Resultate mit  zugehörigen Xpath-Ausdrücken ")
res = dcco.get_calibration_results('xpath', 'de')
for i in res:
   print(i)

#res = dcco.get_calibration_result_by_quantity_refType2('basic_measurementError')
#print("2. function that extracts si-information of a quantity with refType: basic_measurementError")
#print(res)

#res = dcco.get_calibration_result_by_quantity_refType3('basic_measurementError')
#print("3. function that extracts si-information of a quantity with refType: basic_measurementError")
#print(res)


examples/KJ_example.py
siemens/pydcc
# PyDCC
#
# Minimal working example
#
# Copyright (c) Siemens AG, 2023
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
# Important note:
# This server is for testing purpose only.
# It is not recommended to run this server in a productive environment.
#
from flask import Flask

app = Flask(__name__)

@app.route('/dcc/123', methods=['GET'])
def dcc_test_service():
    xml_file_name = "../data/dcc/dcc_gp_temperature_typical_v12.xml"
    with open(xml_file_name, "rb") as file:
        byte_array = file.read()
    return byte_array

if __name__ == '__main__':
    app.run(host='127.0.0.1', debug=True, port=8085)



examples/dcc_test_server.py
siemens/pydcc
# PyDCC
#
# Example script for generating an compressed DCC using PyDCC
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
import sys
sys.path.append("../dcc")
from dcc import DCC

# Choose file stored at ../data/dcc
xml_file_name = 'DCC_3_GrundstrukturPyDCC.xml'

# (1) Load DCC from XML file
xml_file_path = '../data/dcc/' + xml_file_name # Example from PTB
dcco = DCC(xml_file_path) # Load DCC and crate DCC object
calib_date = dcco.calibration_date()
print("Embedded DCC generation for constraint devices")

# (2) Generate compressed DCC
embdcc = dcco.generate_compressed_dcc()
compression_ratio_100 = embdcc['compression_ratio'] * 100
print('DCC size %d bytes' % embdcc['bytes_uncompressed'])
print('Compressed DCC size %d bytes' % embdcc['bytes_compressed'])
print('CRC32 of raw data: %x' % embdcc['crc32'])
print('Embedded DCC compression ratio %.1f%%' % compression_ratio_100)


# (3) Write compressed DCC to file
xml_compressed_Name = xml_file_name.split('.')
compressed_dcc_filename = "../data/compressed_dcc/" + xml_compressed_Name[0] + ".pydcc"
with open(compressed_dcc_filename, "wb") as f:
    f.write(embdcc['dcc_xml_raw_data_compressed'])


# (4) Load compressd DCC from file
with open(compressed_dcc_filename, "rb") as f:
            compressed_dcc_byte_array = f.read()
dcco2 = DCC(compressed_dcc = compressed_dcc_byte_array)
uid = dcco2.uid()
days_since_calibration = dcco2.days_since_calibration()
print('DCC UID: %s' % uid)
print('%d days since calibration' % days_since_calibration)

# TODO use generic name
# (5) Additionally, write header file for C compiler.
c_array = embdcc['compressed_dcc_data_in_c']
#print(c_array[:102] + " ...")
with open("../data/siliziumkugel_compressed.h", "w") as f:
    f.write(c_array)


examples/generate_embedded_dcc.py
siemens/pydcc
# PyDCC
#
# Minimal working example
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
import sys
sys.path.append("../dcc")
from dcc import DCC

# (1) Load DCC and create the DCC object (dcco)
dcco = DCC('../data/dcc/siliziumkugel_2_4_0.xml')

# (2) Get UID of the DCC from DCC object
uid = dcco.uid()

# (3) Get days since last calibration from DCC object
days_since_calibration = dcco.days_since_calibration()

# (4) Print data
print('DCC UID: %s' % uid)
print('%d days since calibration' % days_since_calibration)

examples/minimal_working_example.py
siemens/pydcc
# PyDCC
#
# Python module for processing of digital calibration certificates (DCC)
# according to https://www.ptb.de/dcc/
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
import sys
sys.path.append("../dcc")
from dcc import DCC

xml_file_name = '../data/dcc/dcc_gp_temperature_typical_v12.xml' # Example from PTB
dcco = DCC(xml_file_name) # Load DCC and crate DCC object

if not dcco.status_report.is_loaded:
    print("Error: DCC was not loaded successfully!")

dcco.verify_dcc_xml(online=True)

if dcco.status_report.schema_verification_performed:
    if dcco.status_report.valid_schema:
        print("XML schema is valid.")
    else:
        print("Error: XML schema is invalid.")
else:
    print("Warning: XML schema verification was not performed!")

calib_date = dcco.calibration_date()
print('Calibration date: %s' % calib_date.strftime("%d. %B %Y") )
days_since_calibration = dcco.days_since_calibration()
print('%d days since calibration' % days_since_calibration)
uid = dcco.uid()
print('DCC UID: %s' % uid)

calibration_cycle_in_days = 365
if (days_since_calibration > calibration_cycle_in_days):
    print('Recalibration overdue according to QMS.')
else:
    next_calibration_in_days = calibration_cycle_in_days - days_since_calibration
    print('Next recalibration in %u days according to QMS.' % next_calibration_in_days)

if dcco.status_report.is_signed:
    print('Signature available.')
    if dcco.status_report.valid_signature:
        print('Signature is valid.')
    else:
        print('Error: Signature could not be verified.')
else:
    print('Warning: DCC is not signed.')


status_summary_flag = dcco.status_report.get_status_summary()
if (status_summary_flag):
    print('All test were passed.')
else:
    print('Warning: Some test failed.')
    print(dcco.status_report)
examples/read_common_data_from_dcc.py
siemens/pydcc
# PyDCC
#
# Minimal working example
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
# Before starting this example, make sure to run the test server.
# dcc_test_server.py
#
import sys
sys.path.append("../dcc")
from dcc import DCC

# (1) Load DCC and create the DCC object (dcco)
dcco = DCC(url="http://127.0.0.1:8085/dcc/123")

# (2) Get UID of the DCC from DCC object
uid = dcco.uid()

# (3) Get days since last calibration from DCC object
days_since_calibration = dcco.days_since_calibration()

# (4) Print data
print('DCC UID: %s' % uid)
print('%d days since calibration' % days_since_calibration)

examples/read_data_from_server.py
siemens/pydcc
# PyDCC
#
# Python module for processing of digital calibration certificates (DCC)
# according to https://www.ptb.de/dcc/
#
# Copyright (c) Siemens AG, 2023
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
import sys
sys.path.append("../dcc")
from dcc import DCC

xml_file_name = '../data/dcc/dcc_gp_temperature_typical_v12.xml'
dcco = DCC(xml_file_name) # Load DCC and crate DCC object


print(dcco.item_id())

def print_serial_number(serial_number):
	if serial_number is not None:
		print("Serial number: " + serial_number)
	else:
		print("Serial number not found.")

# A search string can be specified. The first match will be returned.
serial_number = dcco.get_item_id_by_name('Serial no.')
print_serial_number(serial_number)

serial_number = dcco.get_item_id_by_name('Serial no.', 'en')
print_serial_number(serial_number)

serial_number = dcco.get_item_id_by_name('Serial no.', 'en', 'manufacturer')
print_serial_number(serial_number)

serial_number = dcco.get_item_id_by_name('Serien Nr.')
print_serial_number(serial_number)

serial_number = dcco.get_item_id_by_name('Seriennummer')
print_serial_number(serial_number)
examples/read_identification.py
siemens/pydcc
# PyDCC
#
# Example for processing DCC signatures
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
import sys

from dcc.dcc import DCCTrustStore, DCCSignatureError

sys.path.append("../dcc")
from dcc import DCC

# (1) Create trust store and add trusted root and intermediate certificate
trust_store = DCCTrustStore()
trust_store.load_trusted_root_from_file("../data/trusted_certs/root.crt")
trust_store.load_intermediate_from_file("../data/trusted_certs/sub.crt")

try:
    # (1) Load DCC and create the DCC object (dcco)
    dcco = DCC(xml_file_name='../data/dcc/dcc_gp_temperature_typical_v12_v3.2.0_signed_lt.xml', trust_store=trust_store)
    # (2) Verify if DCC is signed
    if not dcco.status_report.is_signed:
        print('DCC is not signed')

    else:
        print('DCC is signed and signature of DCC is valid')

        # (3) Get signing time
        print("Signing time: %s" % dcco.get_signing_time().strftime('%d/%m/%Y %H:%M:%S'))

        # (4) Get signer certificate
        cert = dcco.get_signer_certificate()

        # (5) Get Subject Name of the signer certificate
        print("Subject name of signer certificate: %s" % cert.subject.rfc4514_string())

        # (6) Get validity period of the signer certificate
        print("Signer certificate is valid from: %s" % cert.not_valid_before.strftime('%d/%m/%Y %H:%M:%S'))
        print("Signer certificate is valid to: %s" % cert.not_valid_after.strftime('%d/%m/%Y %H:%M:%S'))


except DCCSignatureError as e:
    print(str(e))







examples/signature_example.py
siemens/pydcc
# PyDCC
#
# Automated uncertainty verification example
#
# Copyright (c) Siemens AG, 2022
#
# Authors:
#     Andreas Tobola
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
# This example simulates a machinally verification of uncertainty values.
# Pretending a new calibration has been executed. Usually, the calibration
# results have to be verified if they meet the requirements for a certain application.
# However, with a DCC this verification can be realized fully automated by computers.

import sys
sys.path.append("../dcc/")
from dcc import DCC
import numpy as np


def search_calibration_results(cres, searched_reftype):
    for elem in cres:
        xpath_string = elem[0]
        if xpath_string.find(searched_reftype) > -1:
            selected_calib_result = elem[1]
            return selected_calib_result


def measurement_error_evaluation(measurement_error, uncertainty, lower_limit, upper_limit):

	lower_limit_extended = lower_limit - uncertainty
	upper_limit_extended = upper_limit + uncertainty

	lower_limit_limited = lower_limit + uncertainty
	upper_limit_limited = upper_limit - uncertainty

	#if upper_limit_limited <= lower_limit_limited:
	#	print("Limits implicit conditional")

	conditional = False

	if measurement_error >= lower_limit and measurement_error <= upper_limit:
		passed = True
		if measurement_error < lower_limit_limited or measurement_error > upper_limit_limited:
			conditional = True
		else:
			conditional = False
	else:
		passed = False
		if measurement_error >= lower_limit_extended and measurement_error <= upper_limit_extended:
			conditional = False
		else:
			conditional = True

	return (passed, conditional)

def xml_list_to_float(xml_list: str):
	string_list = xml_list.split(' ')
	float_list = []
	for item in string_list:
		float_list.append(float(item))
	return float_list


def evaluate_measurements_results_for_given_limits(measurement_error_array, laboratory_measurement_uncertainty, measurement_error_requirement):

	print(" ")
	print(" ")
	print("Evaluation with required limits at +/- %.3f K" % measurement_error_requirement)

	overall_passed = True
	overall_conditional = False
	print ("  MSE   Passed  Cond.")
	for mse in measurement_error_array:
		passed, conditional = measurement_error_evaluation(mse, laboratory_measurement_uncertainty, -measurement_error_requirement, measurement_error_requirement)
		print("%7.3f %5s  %5s" % ( mse, passed, conditional ) )
		overall_passed = overall_passed and passed
		overall_conditional = overall_conditional or conditional


	print(" ")
	print("Overall passed:             %5s" % (overall_passed ) )
	print("Overall conditional:        %5s" % (overall_conditional) )

	device_meets_requirements = overall_passed and not overall_conditional

	print('Device meets requirements:  %5s' % device_meets_requirements)

	return device_meets_requirements


if __name__ == '__main__':


	# Load DCC and create the DCC object (dcco)
	dcco = DCC('../data/dcc/dcc_gp_temperature_typical_v12.xml')

	#lang = dcco.mandatory_language()

	# Get calibration results
	cres = dcco.get_calibration_results('xpath')


	# Select data set with measurement error
	mres = search_calibration_results(cres, 'basic_measurementError')
	print(mres)

	# Select array with measurement results
	measurement_error_array = xml_list_to_float(mres['realListXMLList']['valueXMLList'])

	print(measurement_error_array)

	# Select meaasurement uncertainty
	laboratory_measurement_uncertainty = float(mres['realListXMLList']['expandedUncXMLList']['uncertaintyXMLList'])
	print(" ")
	print("Measurement uncertainty was +/- %.4f K (2*Sigma)" % laboratory_measurement_uncertainty)

	# Execute tests with different requirements
	evaluate_measurements_results_for_given_limits(measurement_error_array, laboratory_measurement_uncertainty, 0.1)
	evaluate_measurements_results_for_given_limits(measurement_error_array, laboratory_measurement_uncertainty, 0.16)
	evaluate_measurements_results_for_given_limits(measurement_error_array, laboratory_measurement_uncertainty, 0.18)

examples/uncertainty_check_example.py
siemens/pydcc
# PyDCC automatic version increment
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#

with open('next_version.txt', 'r') as file:
    current_version = file.read()

version_separator = '.'
version_elements = current_version.split(version_separator)

build_version=int(version_elements[2])
build_version += 1
version_elements[2] = str(build_version)
new_version = version_separator.join(version_elements) 
with open('next_version.txt', 'w') as file:
    file.write(new_version)

increase_version.py
siemens/pydcc
# PyDCC setup script for generating PyDCC package
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#

from setuptools import setup

with open('README.md') as file:
    long_description = file.read()

with open('next_version.txt', 'r') as file:
    current_version = file.read()

setup(
    name="pydcc",
    version=current_version,
    author="Andreas Tobola",
    author_email="pydcc.t@siemens.com",
    description=("Library for handling digital calibration certificates (DCC). "),
    license="MIT License",
    keywords="digital calibration certificate measurement uncertainty precision GUM DCC",
    url="https://github.com/siemens/pydcc",
    packages=['dcc'],
    include_package_data=True,
    long_description=long_description,
    long_description_content_type="text/markdown",
    classifiers=[
        "Development Status :: 3 - Alpha",
        "Topic :: Utilities",
        "License :: OSI Approved :: MIT License",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.8",
        "Operating System :: OS Independent",
    ],
    install_requires=[
        'xmlschema > 1.5', 'requests', 'signxml >= 3.1.0', 'certvalidator', 'asn1crypto', 'cryptography', 'pydantic'
    ]
)

setup.py
siemens/pydcc
# Unit tests for PyDCC
#
# Python module for processing of digital calibration certificates (DCC)
# according to https://www.ptb.de/dcc/
#
# Copyright (c) Siemens AG, 2023
#
# Authors:
#  Andreas Tobola <andreas.tobola@siemens.com>
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#
import sys
sys.path.append("../dcc")
import dcc

functions_DCC = dir(dcc.DCC)

with open('../doc/pydcc.md') as f:
    documentation = f.read()

found_counter = 0
total_counter = 0

for function in functions_DCC:
    if not function[0] == '_':
        total_counter += 1
        if documentation.find(function) > -1:
            found_counter += 1
        else:
            print(function)

print(" ")
print("%u status::documented" % found_counter)
print("%u status::total " % total_counter)
print("%u status::undocumented" % (total_counter-found_counter))


#print("%u of %u functions are documented" % (found_counter, total_counter))





tests/documentation_check.py
siemens/pydcc
# Unit tests for PyDCC
#
# Python module for processing of digital calibration certificates (DCC)
# according to https://www.ptb.de/dcc/
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#     Kai Mienert, PTB
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#

from dcc.dcc_xml_validator import DCCXMLValidator
from dcc.dcc_xml_validator import WORKSPACE_STATUS_OK
from dcc.dcc_xml_validator import get_imports_from_xml
from dcc.dcc_xml_validator import get_target_namespace_from_xml
from dcc.dcc_xml_validator import get_schema_version
import unittest
import xml.etree.ElementTree as ET
import os


class DxxXmlValidatorTest(unittest.TestCase):

    def test_workspcae_valid(self):
        val_obj = DCCXMLValidator(path_to_workspace="data/test_workspace")
        self.assertEqual(WORKSPACE_STATUS_OK, val_obj.workspace_status)
        self.cleanup_workspace("data/test_workspace")

    def test_get_imports_from_xml(self):
        with open("data/test.xsd", mode="r", encoding="utf-8") as file:
            xml_string = file.read()

        schema_locations = get_imports_from_xml(xml_string)
        self.assertEqual(schema_locations, ['schemaLocation1', 'schemaLocation2'])

    def test_get_target_namespace_from_xml(self):
        with open("data/test.xsd", mode="r", encoding="utf-8") as file:
            xml_string = file.read()

        target_namespace = get_target_namespace_from_xml(xml_string)
        self.assertEqual(target_namespace, "namespace1")

    def test_get_schema_version(self):
        with open("../data/dcc/dcc_gp_temperatur_resistance_v12.xml", mode="r", encoding="utf-8") as file:
            xml_string = file.read()

        schema_version = get_schema_version(xml_string)
        self.assertEqual(schema_version, "3.1.1")

    def test_online_schema_validation(self):
        val_obj = DCCXMLValidator(workspace_init=False)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_humidity_v1.0.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.2", online=True)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperatur_resistance_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=True)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperature_extensive_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=True)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperature_simplified_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=True)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperature_typical_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=True)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperature_typical_adjustment_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=True)
            self.assertEqual(valid, True)

    def test_offline_schema_validation(self):
        val_obj = DCCXMLValidator()
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_humidity_v1.0.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.2", online=False)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperatur_resistance_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=False)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperature_extensive_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=False)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperature_simplified_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=False)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperature_typical_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=False)
            self.assertEqual(valid, True)
        with self.subTest():
            dcc_tree = ET.parse("../data/dcc/dcc_gp_temperature_typical_adjustment_v12.xml")
            valid = val_obj.dcc_is_valid_against_schema(dcc_etree=dcc_tree, dcc_version="3.1.1", online=False)
            self.assertEqual(valid, True)

        self.cleanup_workspace("data/test_workspace")

    def cleanup_workspace(self, workspace):
        for f in os.listdir(workspace):
            if "Readme.md" in f:
                continue
            os.remove(os.path.join(workspace, f))


if __name__ == "__main__":
    unittest.main()

tests/loader_test.py
siemens/pydcc
# Unit tests for PyDCC
#
# Python module for processing of digital calibration certificates (DCC)
# according to https://www.ptb.de/dcc/
#
# Copyright (c) Siemens AG, 2021
#
# Authors:
#  Andreas Tobola, Siemens AG
#
# This work is licensed under the terms of the MIT License.
# See the LICENSE file in the top-level directory.
#
# SPDX-License-Identifier:  MIT
#

from dcc import DCCStatusType
from dcc import DCC
import datetime
import unittest
from cryptography import x509
from dcc.dcc import DCCTrustStore
from dcc.dcc import DCCSignatureError
from flask import Flask
from multiprocessing import Process
import time


xml_file_name_gp = 'dcc_gp_temperature_typical_v12.xml'
xml_file_path_gp = '../data/dcc/' + xml_file_name_gp
dcco_gp = DCC(xml_file_path_gp)


app = Flask(__name__)

@app.route('/dcc/123', methods=['GET'])
def dcc_test_service():
    with open(xml_file_path_gp, "rb") as file:
        byte_array = file.read()
    return byte_array

def service_thread():
    global app
    app.run(host='127.0.0.1', debug=False, port=8085)


class TestBaseFunctions(unittest.TestCase):

    def test_loading_from_file(self):
        dcc_from_file = DCC(xml_file_path_gp)  # Load DCC and crate DCC object
        self.assertTrue(dcc_from_file.is_loaded())

    def test_loading_byte_array(self):
        with open(xml_file_path_gp, "rb") as f:
            dcc_byte_array = f.read()
        dcc_from_byte_array = DCC(byte_array=dcc_byte_array)  # Load DCC and crate DCC object
        self.assertTrue(dcc_from_byte_array.is_loaded())

    def test_loading_compressed_byte_array(self):
        compressed_file = xml_file_name_gp.split('.')
        with open("../data/compressed_dcc/" + compressed_file[0] + ".pydcc", "rb") as f:
            dcc_compressed = f.read()

        dcc_from_compressed_byte_array = DCC(compressed_dcc=dcc_compressed)  # Load DCC and crate DCC object
        self.assertTrue(dcc_from_compressed_byte_array.is_loaded())

    def test_loading_form_server(self):

        # Create local server for test purposes only
        tserv = Process(target=service_thread)
        tserv.start()

        # Give Flask some time to get ready. It would be better to ask Flask for being ready.
        time.sleep(4)

        # Load from server
        dcc_from_server = DCC(url="http://127.0.0.1:8085/dcc/123")

        # Terminate server
        tserv.terminate()
        tserv.join()

        self.assertTrue(dcc_from_server.is_loaded())

    def test_mandatoryLang(self):
        lang = dcco_gp.mandatory_language()
        self.assertEqual(lang, 'de')

    def test_get_calibration_results(self):
        res = dcco_gp.get_calibration_results('name')
        self.assertEqual(res[0], [' Messergebnisse Bezugswert', {'hybrid': {'realListXMLList': [{'valueXMLList': '306.248 373.121 448.253 523.319 593.154', 'unitXMLList': '\\kelvin'}, {'valueXMLList': '33.098 99.971 175.103 250.169 320.004', 'unitXMLList': '\\degreecelsius'}]}}])
        self.assertEqual(res[1], [' Messergebnisse Angezeigter Messwert Kalibriergegenstand', {'hybrid': {'realListXMLList': [{'valueXMLList': '306.32 373.21 448.36 523.31 593.07', 'unitXMLList': '\\kelvin'}, {'valueXMLList': '33.17 100.06 175.21 250.16 319.92', 'unitXMLList': '\\degreecelsius'}]}}])
        self.assertEqual(res[2], [' Messergebnisse Messabweichung', {'realListXMLList': {'valueXMLList': '0.072 0.089 0.107 -0.009 -0.084', 'unitXMLList': '\\kelvin', 'expandedUncXMLList': {'uncertaintyXMLList': '0.061', 'coverageFactorXMLList': '2', 'coverageProbabilityXMLList': '0.95', 'distributionXMLList': 'normal'}}}])

        res = dcco_gp.get_calibration_results('xpath')
        self.assertEqual(res[0], [".//dcc:result [ @ refType ='gp_measuringResult1'] //dcc:list [ @ refType ='gp_table1'] //dcc:quantity [ @ refType ='basic_referenceValue']", {'hybrid': {'realListXMLList': [{'valueXMLList': '306.248 373.121 448.253 523.319 593.154', 'unitXMLList': '\\kelvin'}, {'valueXMLList': '33.098 99.971 175.103 250.169 320.004','unitXMLList': '\\degreecelsius'}]}}])
        self.assertEqual(res[1], [".//dcc:result [ @ refType ='gp_measuringResult1'] //dcc:list [ @ refType ='gp_table1'] //dcc:quantity [ @ refType ='basic_measuredValue']", {'hybrid': {'realListXMLList': [{'valueXMLList': '306.32 373.21 448.36 523.31 593.07', 'unitXMLList': '\\kelvin'}, {'valueXMLList': '33.17 100.06 175.21 250.16 319.92', 'unitXMLList': '\\degreecelsius'}]}}])
        self.assertEqual(res[2], [".//dcc:result [ @ refType ='gp_measuringResult1'] //dcc:list [ @ refType ='gp_table1'] //dcc:quantity [ @ refType ='basic_measurementError']", {'realListXMLList': {'valueXMLList': '0.072 0.089 0.107 -0.009 -0.084', 'unitXMLList': '\\kelvin', 'expandedUncXMLList': {'uncertaintyXMLList': '0.061', 'coverageFactorXMLList': '2', 'coverageProbabilityXMLList': '0.95', 'distributionXMLList': 'normal'}}}])

    def test_calibration_date(self):
        calib_date = dcco_gp.calibration_date()
        ref_date = datetime.datetime(1957, 8, 13, 0, 0)
        self.assertEqual(calib_date, ref_date)

    def test_days_since_calibration(self):
        days = dcco_gp.days_since_calibration()
        self.assertTrue(days > 40)

    def test_calibration_laboratory_name(self):
        calib_lab_name = dcco_gp.calibration_laboratory_name()
        ref_lab_name = 'Kalibrierfirma GmbH'
        self.assertEqual(calib_lab_name, ref_lab_name)

    def test_uid(self):
        uid = dcco_gp.uid()
        self.assertEqual(uid, "GP_DCC_temperature_typical_1.2")

    def test_version(self):
        version = dcco_gp.version()
        self.assertEqual(version, "3.1.1")

    def test_empty_dcc_init_error_detection(self):
        exception_rised = False
        try:
            dcc_empty = DCC()
        except Exception:
            exception_rised = True
        self.assertTrue(exception_rised)

    def test_is_not_signed(self):
        self.assertFalse(dcco_gp.status_report.is_signed)

    def test_is_signed(self):
        xml_file_name = '../data/dcc/signed_siliziumkugel.xml'  # Example from PTB and signed by T-Systems
        dcc_signed = DCC(xml_file_name, signature_verification=False)
        self.assertTrue(dcc_signed.status_report.is_signed)

    def test_compressed_dcc_crc(self):
        comp_dcc = dcco_gp.generate_compressed_dcc()
        crc32 = comp_dcc['crc32']
        self.assertEqual(crc32, 4136415575)

    def test_compressed_dcc_size(self):
        comp_dcc = dcco_gp.generate_compressed_dcc()
        bytes_compressed = comp_dcc['bytes_compressed']
        self.assertEqual(bytes_compressed, 4480)

    def test_previous_report_available(self):
        self.assertFalse(dcco_gp.has_previous_report())

    def test_item_id(self):
        id_dict_v3 = {'identifications': {'identification': [{'issuer': 'manufacturer', 'value': 'string-manufacturer-item',
                                                 'name': {'content': [{'@lang': 'de', '#text': 'Serien Nr.'},
                                                                      {'@lang': 'en', '#text': 'Serial no.'}]}},
                                                {'issuer': 'customer', 'value': 'string-customer-item', 'name': {
                                                    'content': [{'@lang': 'de', '#text': 'Messmittel Nr.'},
                                                                {'@lang': 'en',
                                                                 '#text': 'Measurement equipment no.'}]}},
                                                {'issuer': 'calibrationLaboratory',
                                                 'value': 'string-calibrationLaboratory-item', 'name': {
                                                    'content': [{'@lang': 'de', '#text': 'Equipment Nr.'},
                                                                {'@lang': 'en', '#text': 'Equipment no.'}]}}]}}

        self.assertEqual(dcco_gp.item_id(), id_dict_v3)

    def test_get_item_id_by_name(self):
        serial_number = dcco_gp.get_item_id_by_name('Serial no.')
        self.assertEqual(serial_number, 'string-manufacturer-item')

    def test_get_item_id_by_name_with_language(self):
        serial_number = dcco_gp.get_item_id_by_name('Serial no.', 'en')
        self.assertEqual(serial_number, 'string-manufacturer-item')

    def test_get_item_id_by_name_with_language_and_issuer(self):
        serial_number = dcco_gp.get_item_id_by_name('Serial no.', 'en', 'manufacturer')
        self.assertEqual(serial_number, 'string-manufacturer-item')

    def test_get_item_id_by_name_none(self):
        serial_number = dcco_gp.get_item_id_by_name('Serial no.' ,'de')
        self.assertEqual(serial_number, None)

    def test_verify_correct_dcc_xml_online(self):
        self.assertTrue(dcco_gp.verify_dcc_xml(online=True))

    def test_verify_correct_dcc_xml_offline(self):
        self.assertTrue(dcco_gp.verify_dcc_xml(online=False))

    def test_verify_incorrect_dcc_xml_online(self):
        xml_file_name_wrong_schema = '../data/siliziumkugel_wrong_schema.xml'# Example from PTB
        dcco_wrong_schema = DCC(xml_file_name_wrong_schema)
        self.assertFalse(dcco_wrong_schema.verify_dcc_xml(online=True))

    def test_verify_incorrect_dcc_xml_offline(self):
        xml_file_name_wrong_schema = '../data/siliziumkugel_wrong_schema.xml'# Example from PTB
        dcco_wrong_schema = DCC(xml_file_name_wrong_schema)
        self.assertFalse(dcco_wrong_schema.verify_dcc_xml(online=False))

    def test_status_summary(self):
        result = dcco_gp.status_report.get_status_summary()
        self.assertFalse(result)

    def test_status_summary_ignore_list(self):
        result = dcco_gp.status_report.get_status_summary(ignore_list=[DCCStatusType.VALID_SCHEMA, DCCStatusType.IS_SIGNED, DCCStatusType.VALID_SIGNATURE])
        self.assertTrue(result)

    def test_valid_signature(self):
        trust_store = DCCTrustStore()
        trust_store.load_trusted_root_from_file("../data/trusted_certs/root.crt")
        trust_store.load_intermediate_from_file("../data/trusted_certs/sub.crt")
        dcco = DCC(xml_file_name='../data/dcc/dcc_gp_temperature_typical_v12_v3.2.0_signed.xml', trust_store=trust_store)
        self.assertTrue(dcco.status_report.valid_signature)

    def test_invalid_signature(self):
        trust_store = DCCTrustStore()
        trust_store.load_trusted_root_from_file("../data/trusted_certs/root.crt")
        trust_store.load_intermediate_from_file("../data/trusted_certs/sub.crt")
        with self.assertRaises(DCCSignatureError):
            DCC(xml_file_name='../data/dcc/dcc_gp_temperature_typical_v12_v3.2.0_signed_manipulated.xml',
                trust_store=trust_store)

    def test_get_signing_time(self):
        trust_store = DCCTrustStore()
        trust_store.load_trusted_root_from_file("../data/trusted_certs/root.crt")
        trust_store.load_intermediate_from_file("../data/trusted_certs/sub.crt")
        dcco = DCC(xml_file_name='../data/dcc/dcc_gp_temperature_typical_v12_v3.2.0_signed.xml', trust_store=trust_store)
        expected_time = datetime.datetime.fromisoformat('2023-03-27T15:14:30Z'.replace('Z', '+00:00'))
        self.assertEqual(dcco.get_signing_time(), expected_time)

    def test_get_signing_certificate(self):
        trust_store = DCCTrustStore()
        trust_store.load_trusted_root_from_file("../data/trusted_certs/root.crt")
        trust_store.load_intermediate_from_file("../data/trusted_certs/sub.crt")
        dcco = DCC(xml_file_name='../data/dcc/dcc_gp_temperature_typical_v12_v3.2.0_signed.xml', trust_store=trust_store)
        with open('../data/trusted_certs/signer.crt', "rb") as file:
            expected_cert_bytes = file.read()
            expected_cert = x509.load_pem_x509_certificate(expected_cert_bytes)
            self.assertEqual(dcco.get_signer_certificate(), expected_cert)

    def test_revoked_signer(self):
        trust_store = DCCTrustStore()
        trust_store.load_trusted_root_from_file("../data/trusted_certs/root.crt")
        trust_store.load_intermediate_from_file("../data/trusted_certs/sub.crt")
        with self.assertRaises(DCCSignatureError):
            DCC(xml_file_name='../data/dcc/dcc_gp_temperature_typical_v12_v3.2.0_signed_lt_revoked.xml',
                trust_store=trust_store)


if __name__ == '__main__':
    unittest.main()

tests/unit_test.py
siemens/pydcc