From 163e3f3cd50d605da12b003ce56b9f1ee6304aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Wed, 3 Dec 2025 17:42:21 +0100 Subject: [PATCH 01/21] Initialized documentation --- LICENSE | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 31 ++++++- 2 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8bf7333 --- /dev/null +++ b/LICENSE @@ -0,0 +1,251 @@ +European Space Agency Public License (ESA-PL) Permissive – v2.3 + + 1 Definitions + 1.1 "Contributor" means + (a) the individual or legal entity that originally creates + or later modifies the Software and + (b) each subsequent individual or legal entity that creates + or contributes to the creation of Modifications. + 1.2 "Contributor Version" means the version of the Software on which + the Contributor based its Modifications. + 1.3 "Distribution" and "Distribute" means any act of selling, giving, + lending, renting, distributing, communicating, transmitting, + or otherwise making available, physically or electronically + or by any other means, copies of the Software or Modifications. + 1.4 "ESA" means the European Space Agency. + 1.5 "License" means this document. + 1.6 "Licensor" means the individual or legal entity + that Distributes the Software under the License to You. + 1.7 "Modification" means any work or software created that is based + upon or derived from the Software (or portions thereof) or + a modification of the Software (or portions thereof). + For the avoidance of doubt, linking a library to the Software + results in a Modification. + 1.8 "Object Code" means any non-Source Code form of the Software + and/or Modifications. + 1.9 "Patent Claims" (of a Contributor) means any patent claim(s), + owned at the time of the Distribution or subsequently acquired, + including without limitation, method, process and apparatus claims, + in any patent licensable by a Contributor which would be + infringed by making use of the rights granted under Sec. 2.1, + including but not limited to make, have made, use, sell, + offer for sale or import of the Contributor Version + and/or such Contributor's Modifications (if any), either alone + or in combination with the Contributor Version. + "Licensable" means having the right to grant, + whether at the time of the Distribution or subsequently acquired, + the rights conveyed herein. + 1.10 "Software" means the software Distributed under this License + by the Licensor, in Source Code and/or Object Code form. + 1.11 "Source Code" means the preferred, usually human readable form of + the Software and/or Modifications in which modifications are made + and the associated documentation included in or with such code. + 1.12 "You" means an individual or legal entity exercising rights under + this License (the licensee). + + + 2 Grant of Rights + 2.1 Copyright. The Licensor, and each Contributor in respect of such + Contributor's Modifications, hereby grants You a world-wide, + royalty-free, non-exclusive license under Copyright, subject to + the terms and conditions of this License, to: + * use the Software; + * reproduce the Software by any or all means and in any + or all form; + * Modify the Software and create works based on the Software; + * communicate to the public, including making available, display + or perform the Software or copies thereof to the public; + * Distribute, sublicense, lend and rent the Software. + The license grant is perpetual and irrevocable, unless terminated + pursuant to Sec. 7. + 2.2 Patents. Each Contributor in respect of such + Contributor's Modifications, hereby grants You a world-wide, + royalty-free, non-exclusive, sub-licensable license under + Patent Claims to the extent necessary to make use of the rights + granted under Sec. 2.1, including but not limited to make, + have made, use, sell, offer for sale, import, export and Distribute + such Contributor's Modifications and the combination of such + Contributor's Modifications with the Contributor Version + (collectively called the "Patent Licensed Version" of the Software). + No patent license is granted for claims that are infringed: + * only as a consequence of further modification of + the Patent Licensed Version; or + * by the combination of the Patent Licensed Version with other + software or other devices or hardware, unless such combination + was an intended use case of the Patent Licensed Version + (e.g. a general purpose library is intended to be used with + other software, a satellite navigation software is intended to + be used with appropriate hardware); or + * by a Modification under Patent Claims in the absence of + the Contributor's Modifications or by a combination of + the Contributor's Modifications with software other than + the Patent Licensed Version or Modifications thereof. + 2.3 Trademark. This License does not grant permission to use + trade names, trademarks, services marks, logos or names of + the Licensor, except as required for reasonable and customary use in + describing the origin of the Software and as reasonable necessary + to comply with the obligations of this License (e.g. by reproducing + the content of the notices). For the avoidance of doubt, + upon Distribution of Modifications You must not use the Licensor's + or ESA's trademarks, names or logos in any way that states or + implies, or can be interpreted as stating or implying, that + the final product is endorsed or created by the Licensor or ESA. + + + 3 Distribution + 3.1 No Copyleft. + You may Distribute the Software and/or Modifications, as Source Code + or Object Code, under any license terms, provided that + (a) notice is given of the use of the Software and the applicability + of this License to the Software; and + (b) You make best efforts to ensure that further Distribution of + the Software and/or Modifications (including further + Modifications) is subject to the obligations set forth in this + Sec. 3.1 (a) and (b). + + + 4 Notices + The following obligations apply in the event of any Distribution of + the Software and/or Modifications as Source Code and/or Object Code: + 4.1 You must include a copy of this License and all of the notices + set out in this Sec. 4. + 4.2 You may not remove or alter any copyright, patent, trademark + and attribution notices nor any of the notices set out in this + Sec. 4, except as necessary for your compliance with this License + or otherwise permitted by this License, except for those notices + that do not pertain to the Modifications You Distribute. + 4.3 Each Contributor must cause its Modification carrying prominent + notices stating that the Software has been modified and + the date of modification and identify itself as the originator + of its Modifications in a manner that reasonably allows + identification and contact with the Contributor. + The aforementioned notices must at a minimum be in a text file + included with the Distribution titled "CHANGELOG". + 4.4 The Software may include a "NOTICE" text file containing general + notices. Any Contributor can create such a NOTICE file or add + notices to it, alongside or as an addendum to the original text, + provided that such notices cannot be construed as modifying + the License. + 4.5 Each Contributor must identify all of its Patent Claims by providing + at a minimum the patent number and identification and contact + information in a text file included with the Distribution + titled "LEGAL". + + 5 Warranty and Liability + 5.1 Each Contributor warrants and represents that it has sufficient + rights to grant the rights to its Modifications conveyed + by this License. + 5.2 Except as expressly set forth in this License, + the Software is provided to You on an "as is" basis + and without warranties of any kind, including without limitation + merchantability, fitness for a particular purpose, absence of + defects or errors, accuracy or non-infringement of intellectual + property rights. Mandatory statutory warranty claims, + e.g. in the event of wilful deception or fraudulent + misrepresentation, shall remain unaffected. + 5.3 Except as expressly set forth in this License, + neither Licensor nor any Contributor shall be liable, including, + without limitation, for direct, indirect, incidental, + or consequential damages (including without limitation + loss of profit), however caused and on any theory of liability, + arising in any way out of the use or Distribution of the Software + or the exercise of any rights under this License, even if You have + been advised of the possibility of such damages. Mandatory statutory + liability claims, e.g. in the event of wilful misconduct, wilful + deception or fraudulent misrepresentation, shall remain unaffected. + + 6 Additional Agreements + While Distributing the Software or Modifications, You may choose + to conclude additional agreements, for free or for charge, regarding + for example support, warranty, indemnity, liability or liability + obligations and/or rights, provided such additional agreements are + consistent with this License and do not effectively restrict + the recipient's rights under this License. However, in accepting such + obligations, You may act only on Your own behalf and on Your sole + responsibility, not on behalf of any other Contributor or Licensor, + and only if You agree to indemnify, defend, and hold each Contributor + or Licensor harmless for any liability incurred by, or claims asserted + against, such Contributor or Licensor by reason of your accepting + any such warranty or additional liability. + + 7 Infringements + 7.1 If You have knowledge that exercising rights granted by this License + infringes third party's intellectual property rights, including + without limitation copyright and patent rights, You must take + reasonable steps (such as notifying appropriate mailing lists + or newsgroups) to inform ESA and those who received the Software + about the infringement. + 7.2 You acknowledge that continuing to use the Software knowing + that such use infringes third party rights (e.g. after receiving + a third party notification of infringement) would expose you to the + risk of being considered as intentionally infringing third party + rights. In such event You should acquire the respective rights + or modify the Software so that the Modification is non-infringing. + + + 8 Termination + 8.1 This License and the rights granted hereunder will terminate + automatically upon any breach by You with the terms of this License + if you fail to cure such breach within 30 days of becoming aware of + the breach. + 8.2 If You institute patent litigation against any entity (including + a cross-claim or counterclaim in a lawsuit) alleging that + the Software constitutes direct or contributory patent infringement, + then any patent and copyright licenses granted to You under this + License for the Software shall terminate as of the date such + litigation is filed. + 8.3 Any licenses validly granted by You under the License prior + to termination shall continue and survive termination. + + + 9 Applicable Law, Arbitration and Compliance + 9.1 This License is governed by the laws of the ESA Member State where + the Licensor resides or has his registered office. "Member States" + are the members of the European Space Agency pursuant to Art. 1 of + the ESA Convention*. This licence shall be governed by German law if + a dispute arises with the ESA as a Licensor or if the Licensor has + no residence or registered office inside a Member State. + 9.2 Any dispute arising out of this License shall be finally settled in + accordance with the Rules of Arbitration of the International + Chamber of Commerce by one or more arbitrators designated in + conformity with those rules. Arbitration proceedings shall take + place in Cologne, Germany. The award shall be final and binding on + the parties, no appeal shall lie against it. The enforcement of the + award shall be governed by the rules of procedure in force in the + state/country in which it is to be executed. + 9.3 For the avoidance of doubt, You are solely responsible for + compliance with current applicable requirements of national laws. + The Software can be subject to export control laws. If You export + the Software it is your responsibility to comply with all export + control laws. This may include different requirements, + as e.g. registering the Software with the local authorities. + 9.4 If it is impossible for You to comply with any of the terms of this + License due to statute, judicial order or regulation You must: + (a) comply with the terms of this License to the maximum extent + possible; and + (b) describe the limitations and the Object Code and/or Source Code + they affect. Such description must be included in the LEGAL + notice described in Section 4. Except to the extent prohibited + by statute or regulation, such description must be sufficiently + detailed for an average recipient to be able to understand it. + + + 10 Miscellaneous + 10.1 Only ESA has the right to modify or publish new versions of this + License. ESA may assign this right to other individuals or + legal entities. Each version will be given a distinguishing + version number. + 10.2 This License represents the complete agreement concerning subject + matter hereof. + 10.3 If any provision of this License is held invalid or unenforceable, + the remaining provisions of this License shall not be affected. + The invalid or unenforceable provision shall be construed and/or + reformed to the extent necessary to make it enforceable and valid. + + +______________________________ + +*: As of August 2017 the Member States are Austria, Belgium, Czech Republic, + Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, + Luxembourg, The Netherlands, Norway, Poland, Portugal, Romania, Spain, + Sweden, Switzerland and the United Kingdom. \ No newline at end of file diff --git a/README.md b/README.md index 7c57fd5..b202f28 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# template-processor -Template processing engine developed for TASTE Document Generator +# Template Processor + +## General + +Template Processor (TP), created as a part of \"Model-Based Execution Platform for Space Applications\" project (contract 4000146882/24/NL/KK) financed by the European Space Agency. + +TP is a template processing engine developed for TASTE Document Generator. It main function is to consume the provided inputs (e.g., TASTE Interface View data), instantiate templates and translate them into format that can be integrated deliverable documents. Base requirements are provided in MBEP-N7S-EP-SRS, while the overall design is documented in MBEP-N7S-EP-SDD. + +## Installation + +TODO + +## Configuration + +None + +## Running + +The assumed use case is for the Template Processor to be invoked by TASTE Document Generator. However, if TP is to be used manually, the following command line interface, as documented in the built-in help, is available: + +TODO + +## Frequently Asked Questions (FAQ) + +None + +## Troubleshooting + +None From 722d567dfeb0050d4b0f0fcb1b76db0226140702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Wed, 3 Dec 2025 18:04:41 +0100 Subject: [PATCH 02/21] Initialized python project --- MANIFEST.in | 7 +++++ Makefile | 30 +++++++++++++++++++ requirements.txt | 4 +++ setup.py | 55 +++++++++++++++++++++++++++++++++++ templateprocessor/__init__.py | 12 ++++++++ templateprocessor/cli.py | 45 ++++++++++++++++++++++++++++ tests/__init__.py | 3 ++ 7 files changed, 156 insertions(+) create mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 templateprocessor/__init__.py create mode 100644 templateprocessor/cli.py create mode 100644 tests/__init__.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2f7edb8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include README.md +include LICENSE +include requirements.txt +recursive-include templateprocessor *.py +recursive-exclude tests * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2114965 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +BLACK=black +PYTHON= ?= python3 + +.PHONY : \ + check \ + all \ + install \ + clean \ + check-format \ + format + +all: check-format check + +install: + pipx install . + +check: + $(MAKE) -C tests check + +check-format: + $(BLACK) --version + $(BLACK) --check templateprocessor + $(BLACK) --check tests + +format: + $(BLACK) templateprocessor + $(BLACK) tests + +clean: + rm -r -f build \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0990c86 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# Core dependencies for Template Processor +# Template processing engine for TASTE Document Generator + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ec90987 --- /dev/null +++ b/setup.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Setup script for Template Processor + +Template Processor (TP), created as a part of "Model-Based Execution Platform +for Space Applications" project (contract 4000146882/24/NL/KK) financed by +the European Space Agency. +""" + +from setuptools import setup, find_packages +from pathlib import Path + +# Read the contents of README file +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text(encoding='utf-8') + +setup( + name='template-processor', + version='0.0.1', + description='Template processing engine for TASTE Document Generator', + long_description=long_description, + long_description_content_type='text/markdown', + author='N7 Space', + author_email='mkurowski@n7space.com', + url='https://github.com/n7space/template-processor', + license='European Space Agency Public License (ESA-PL) Permissive – v2.3', + packages=find_packages(exclude=['tests', 'tests.*']), + include_package_data=True, + python_requires='>=3.8', + install_requires=[ + # Add project dependencies here + ], + extras_require={ + 'dev': [ + 'pytest>=7.0.0', + 'pytest-cov>=4.0.0', + 'flake8>=6.0.0', + 'black>=23.0.0', + 'mypy>=1.0.0', + ], + }, + entry_points={ + 'console_scripts': [ + 'template-processor=templateprocessor.cli:main', + ], + }, + classifiers=[ + 'Programming Language :: Python', + 'License :: ESA-PL Permissive v2.3', + 'Operating System :: Linux' + ], + zip_safe=False, +) diff --git a/templateprocessor/__init__.py b/templateprocessor/__init__.py new file mode 100644 index 0000000..d4516c6 --- /dev/null +++ b/templateprocessor/__init__.py @@ -0,0 +1,12 @@ +""" +Template Processor + +Template processing engine for TASTE Document Generator. +Created as a part of "Model-Based Execution Platform for Space Applications" +project (contract 4000146882/24/NL/KK) financed by the European Space Agency. +""" + +__version__ = "0.0.1" +__author__ = "N7 Space" + +__all__ = ["TemplateProcessor"] diff --git a/templateprocessor/cli.py b/templateprocessor/cli.py new file mode 100644 index 0000000..6884b35 --- /dev/null +++ b/templateprocessor/cli.py @@ -0,0 +1,45 @@ +""" +Command Line Interface for Template Processor +""" + +import argparse +import sys +from templateprocessor import __version__ + + +def main(): + """Main entry point for the Template Processor CLI.""" + parser = argparse.ArgumentParser( + description="Template Processor - Template processing engine for TASTE Document Generator", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--version", action="version", version=f"template-processor {__version__}" + ) + + parser.add_argument( + "-i", + "--input", + help="Input data file (e.g., TASTE Interface View data)", + metavar="FILE", + ) + + parser.add_argument( + "-t", "--template", help="Template file to process", metavar="FILE" + ) + + parser.add_argument( + "-o", "--output", help="Output file for processed template", metavar="FILE" + ) + + args = parser.parse_args() + + print("Template Processor - Not yet implemented") + print(f"Version: {__version__}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..6673ced --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for Template Processor +""" From 739744828e4a844b6ad9642384c71708ed95d703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Wed, 3 Dec 2025 20:51:51 +0100 Subject: [PATCH 03/21] Initial implementation of IV reader --- data/simple.iv.png | Bin 0 -> 53219 bytes data/simple.iv.xml | 195 ++++++++++++++++++++++ templateprocessor/__init__.py | 5 +- templateprocessor/iv.py | 210 ++++++++++++++++++++++++ templateprocessor/ivreader.py | 295 ++++++++++++++++++++++++++++++++++ tests/Makefile | 11 ++ tests/test_ivreader.py | 251 +++++++++++++++++++++++++++++ 7 files changed, 966 insertions(+), 1 deletion(-) create mode 100644 data/simple.iv.png create mode 100644 data/simple.iv.xml create mode 100644 templateprocessor/iv.py create mode 100644 templateprocessor/ivreader.py create mode 100644 tests/Makefile create mode 100644 tests/test_ivreader.py diff --git a/data/simple.iv.png b/data/simple.iv.png new file mode 100644 index 0000000000000000000000000000000000000000..34396b6628a837380f415b79a043a3de119c63a7 GIT binary patch literal 53219 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 literal 0 HcmV?d00001 diff --git a/data/simple.iv.xml b/data/simple.iv.xml new file mode 100644 index 0000000..cc2f58c --- /dev/null +++ b/data/simple.iv.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templateprocessor/__init__.py b/templateprocessor/__init__.py index d4516c6..27d27ac 100644 --- a/templateprocessor/__init__.py +++ b/templateprocessor/__init__.py @@ -9,4 +9,7 @@ __version__ = "0.0.1" __author__ = "N7 Space" -__all__ = ["TemplateProcessor"] +from templateprocessor.ivreader import IVReader +from templateprocessor.iv import InterfaceView + +__all__ = ["IVReader", "InterfaceView"] diff --git a/templateprocessor/iv.py b/templateprocessor/iv.py new file mode 100644 index 0000000..8f78508 --- /dev/null +++ b/templateprocessor/iv.py @@ -0,0 +1,210 @@ +""" +TASTE Interface View (IV) data model classes. + +This module provides Python classes that reflect the schema/structure of +TASTE Interface View XML files, allowing for parsing, manipulation, and +generation of IV data. +""" + +from dataclasses import dataclass, field, fields +from typing import List, Optional, Literal +from enum import Enum + + +class InterfaceKind(str, Enum): + """Types of interfaces in TASTE.""" + + SPORADIC = "Sporadic" + CYCLIC = "Cyclic" + PROTECTED = "Protected" + UNPROTECTED = "Unprotected" + + +class Encoding(str, Enum): + """Parameter encoding types.""" + + NATIVE = "NATIVE" + UPER = "UPER" + ACN = "ACN" + + +class Language(str, Enum): + """Programming languages supported in TASTE.""" + + SDL = "SDL" + C = "C" + ADA = "Ada" + CPP = "C++" + SIMULINK = "Simulink" + QGenc = "QGenc" + + +@dataclass +class Property: + """Generic property/attribute with name-value pair.""" + + name: str + value: str + + +@dataclass +class InterfaceParameter: + """Parameter for an interface.""" + + name: str + type: str + encoding: Encoding = Encoding.NATIVE + + +@dataclass +class InputParameter(InterfaceParameter): + """Input parameter for an interface.""" + + pass + + +@dataclass +class OutputParameter(InterfaceParameter): + """Output parameter for an interface.""" + + pass + + +@dataclass +class FunctionInterface: + """Function interface.""" + + id: str + name: str + kind: InterfaceKind + enable_multicast: bool = True + layer: str = "default" + required_system_element: bool = False + is_simulink_interface: bool = False + simulink_full_interface_ref: str = "" + wcet: int = 0 + stack_size: Optional[int] = None + queue_size: Optional[int] = None + miat: Optional[int] = None + period: Optional[int] = None + dispatch_offset: Optional[int] = None + priority: Optional[int] = None + input_parameters: List[InputParameter] = field(default_factory=list) + output_parameters: List[OutputParameter] = field(default_factory=list) + properties: List[Property] = field(default_factory=list) + + +@dataclass +class ProvidedInterface(FunctionInterface): + """Provided interface - implemented by a function""" + + pass + + +@dataclass +class RequiredInterface(FunctionInterface): + """Required interface - required by a function""" + + pass + + +@dataclass +class Implementation: + """Function implementation details.""" + + name: str + language: Language + + +@dataclass +class Function: + """TASTE Function - a software component in the system.""" + + id: str + name: str + is_type: bool + language: Language + default_implementation: str = "default" + fixed_system_element: bool = False + required_system_element: bool = False + instances_min: int = 1 + instances_max: int = 1 + startup_priority: int = 1 + instance_of: Optional[str] = None # For instances of function types + type_language: Optional[Language] = None # For function types + provided_interfaces: List[ProvidedInterface] = field(default_factory=list) + required_interfaces: List[RequiredInterface] = field(default_factory=list) + implementations: List[Implementation] = field(default_factory=list) + properties: List[Property] = field(default_factory=list) + nested_functions: List["Function"] = field(default_factory=list) + nested_connections: List["Connection"] = field(default_factory=list) + + +@dataclass +class ConnectionEndpoint: + """Connection endpoint.""" + + iface_id: str + function_name: str + iface_name: str + + +@dataclass +class ConnectionSource(ConnectionEndpoint): + """Source endpoint of a connection.""" + + pass + + +@dataclass +class ConnectionTarget(ConnectionEndpoint): + """Target endpoint of a connection.""" + + pass + + +@dataclass +class Connection: + """Connection between two interfaces.""" + + id: str + required_system_element: bool = False + name: str = None + source: ConnectionSource = None + target: ConnectionTarget = None + + +@dataclass +class Comment: + """Comment/annotation in the interface view.""" + + id: str + name: str + required_system_element: bool = False + + +@dataclass +class Layer: + """Visual layer for organizing the interface view.""" + + name: str + is_visible: bool = True + + +@dataclass +class InterfaceView: + """ + Root element representing a TASTE Interface View. + + This is the main data structure that contains all functions, connections, + and other elements that define a TASTE system's interface architecture. + """ + + version: str + asn1file: str + UiFile: str + modifierHash: str + functions: List[Function] = field(default_factory=list) + connections: List[Connection] = field(default_factory=list) + comments: List[Comment] = field(default_factory=list) + layers: List[Layer] = field(default_factory=list) diff --git a/templateprocessor/ivreader.py b/templateprocessor/ivreader.py new file mode 100644 index 0000000..a622d6d --- /dev/null +++ b/templateprocessor/ivreader.py @@ -0,0 +1,295 @@ +""" +TASTE Interface View XML Reader. + +This module provides functionality to parse TASTE Interface View XML files +and construct InterfaceView data model instances. +""" + +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Union, Optional + +from templateprocessor.iv import ( + InterfaceView, + Function, + FunctionInterface, + ProvidedInterface, + RequiredInterface, + InterfaceParameter, + InputParameter, + OutputParameter, + Implementation, + Connection, + ConnectionSource, + ConnectionTarget, + Comment, + Layer, + Property, + Language, + Encoding, + InterfaceKind, +) + + +class IVReader: + """ + Reader for TASTE Interface View XML files. + + Parses XML files conforming to the TASTE Interface View schema and + constructs corresponding InterfaceView objects. + + Example: + reader = IVReader() + interface_view = reader.read("simple.iv.xml") + """ + + def read(self, file_path: Union[str, Path]) -> InterfaceView: + """ + Read and parse a TASTE Interface View XML file. + + Args: + file_path: Path to the IV XML file + + Returns: + InterfaceView object populated with parsed data + + Raises: + FileNotFoundError: If the file does not exist + xml.etree.ElementTree.ParseError: If XML is malformed + """ + file_path = Path(file_path) + if not file_path.exists(): + raise FileNotFoundError(f"Interface View file not found: {file_path}") + + tree = ET.parse(file_path) + root = tree.getroot() + + return self._parse_interface_view(root) + + def read_string(self, xml_content: str) -> InterfaceView: + """ + Read and parse TASTE Interface View XML from a string. + + Args: + xml_content: XML content as string + + Returns: + InterfaceView object populated with parsed data + + Raises: + xml.etree.ElementTree.ParseError: If XML is malformed + """ + root = ET.fromstring(xml_content) + return self._parse_interface_view(root) + + def _parse_interface_view(self, root: ET.Element) -> InterfaceView: + """Parse the root InterfaceView element.""" + iv = InterfaceView( + version=root.get("version", ""), + asn1file=root.get("asn1file", ""), + UiFile=root.get("UiFile", ""), + modifierHash=root.get("modifierHash", ""), + ) + + # Parse all Function elements + for func_elem in root.findall("Function"): + function = self._parse_function(func_elem) + iv.functions.append(function) + + # Parse all Connection elements + for conn_elem in root.findall("Connection"): + connection = self._parse_connection(conn_elem) + iv.connections.append(connection) + + # Parse all Comment elements + for comment_elem in root.findall("Comment"): + comment = self._parse_comment(comment_elem) + iv.comments.append(comment) + + # Parse all Layer elements + for layer_elem in root.findall("Layer"): + layer = self._parse_layer(layer_elem) + iv.layers.append(layer) + + return iv + + def _parse_function(self, elem: ET.Element) -> Function: + """Parse a Function element.""" + function = Function( + id=elem.get("id", ""), + name=elem.get("name", ""), + is_type=elem.get("is_type", "NO") == "YES", + language=Language(elem.get("language", "")) + if elem.get("language") + else None, + default_implementation=elem.get("default_implementation", "default"), + fixed_system_element=elem.get("fixed_system_element", "NO") == "YES", + required_system_element=elem.get("required_system_element", "NO") == "YES", + instances_min=int(elem.get("instances_min", "1")), + instances_max=int(elem.get("instances_max", "1")), + startup_priority=int(elem.get("startup_priority", "1")), + instance_of=elem.get("instance_of"), + type_language=Language(elem.get("type_language")) + if elem.get("type_language") + else None, + ) + + # Parse properties + for prop_elem in elem.findall("Property"): + prop = self._parse_property(prop_elem) + function.properties.append(prop) + + # Parse provided interfaces + for pi_elem in elem.findall("Provided_Interface"): + pi = self._parse_provided_interface(pi_elem) + function.provided_interfaces.append(pi) + + # Parse required interfaces + for ri_elem in elem.findall("Required_Interface"): + ri = self._parse_required_interface(ri_elem) + function.required_interfaces.append(ri) + + # Parse implementations + implementations_elem = elem.find("Implementations") + if implementations_elem is not None: + for impl_elem in implementations_elem.findall("Implementation"): + impl = self._parse_implementation(impl_elem) + function.implementations.append(impl) + + # Parse nested functions + for nested_elem in elem.findall("Function"): + nested_function = self._parse_function(nested_elem) + function.nested_functions.append(nested_function) + + # Parse nested connections + for nested_conn_elem in elem.findall("Connection"): + nested_connection = self._parse_connection(nested_conn_elem) + function.nested_connections.append(nested_connection) + + return function + + def _parse_interface(self, elem: ET.Element) -> FunctionInterface: + """Parse an interface element.""" + iface = FunctionInterface( + id=elem.get("id", ""), + name=elem.get("name", ""), + kind=InterfaceKind(elem.get("kind", "")), + enable_multicast=elem.get("enable_multicast", "true") == "true", + layer=elem.get("layer", "default"), + required_system_element=elem.get("required_system_element", "NO") == "YES", + is_simulink_interface=elem.get("is_simulink_interface", "false") == "true", + simulink_full_interface_ref=elem.get("simulink_full_interface_ref", ""), + wcet=int(elem.get("wcet", "0")), + stack_size=int(elem.get("stack_size")) if elem.get("stack_size") else None, + queue_size=int(elem.get("queue_size")) if elem.get("queue_size") else None, + miat=int(elem.get("miat")) if elem.get("miat") else None, + period=int(elem.get("period")) if elem.get("period") else None, + dispatch_offset=int(elem.get("dispatch_offset")) + if elem.get("dispatch_offset") + else None, + priority=int(elem.get("priority")) if elem.get("priority") else None, + ) + + # Parse input parameters + for param_elem in elem.findall("Input_Parameter"): + param = self._parse_input_parameter(param_elem) + iface.input_parameters.append(param) + + # Parse output parameters + for param_elem in elem.findall("Output_Parameter"): + param = self._parse_output_parameter(param_elem) + iface.output_parameters.append(param) + + # Parse properties + for prop_elem in elem.findall("Property"): + prop = self._parse_property(prop_elem) + iface.properties.append(prop) + + return iface + + def _parse_provided_interface(self, elem: ET.Element) -> ProvidedInterface: + """Parse a Provided_Interface element.""" + pi = ProvidedInterface(**vars(self._parse_interface(elem))) + return pi + + def _parse_required_interface(self, elem: ET.Element) -> RequiredInterface: + """Parse a Required_Interface element.""" + ri = RequiredInterface(**vars(self._parse_interface(elem))) + return ri + + def _parse_parameter(self, elem: ET.Element) -> InterfaceParameter: + """Parse an InterfaceParameter element.""" + return InterfaceParameter( + name=elem.get("name", ""), + type=elem.get("type", ""), + encoding=Encoding(elem.get("encoding", "NATIVE")), + ) + + def _parse_input_parameter(self, elem: ET.Element) -> InputParameter: + """Parse an Input_Parameter element.""" + return InputParameter(**vars(self._parse_parameter(elem))) + + def _parse_output_parameter(self, elem: ET.Element) -> OutputParameter: + """Parse an Output_Parameter element.""" + return OutputParameter(**vars(self._parse_parameter(elem))) + + def _parse_implementation(self, elem: ET.Element) -> Implementation: + """Parse an Implementation element.""" + return Implementation( + name=elem.get("name", ""), + language=elem.get("language", ""), + ) + + def _parse_connection(self, elem: ET.Element) -> Connection: + """Parse a Connection element.""" + connection = Connection( + id=elem.get("id", ""), + required_system_element=elem.get("required_system_element", "NO"), + name=elem.get("name"), + ) + + # Parse source + source_elem = elem.find("Source") + if source_elem is not None: + pi_name = source_elem.get("pi_name") + ri_name = source_elem.get("ri_name") + connection.source = ConnectionSource( + iface_id=source_elem.get("iface_id", ""), + function_name=source_elem.get("func_name", ""), + iface_name=pi_name if pi_name is not None else ri_name, + ) + + # Parse target + target_elem = elem.find("Target") + if target_elem is not None: + pi_name = target_elem.get("pi_name") + ri_name = target_elem.get("ri_name") + connection.target = ConnectionTarget( + iface_id=target_elem.get("iface_id", ""), + function_name=target_elem.get("func_name", ""), + iface_name=pi_name if pi_name is not None else ri_name, + ) + + return connection + + def _parse_comment(self, elem: ET.Element) -> Comment: + """Parse a Comment element.""" + return Comment( + id=elem.get("id", ""), + name=elem.get("name", ""), + required_system_element=elem.get("required_system_element", "NO"), + ) + + def _parse_layer(self, elem: ET.Element) -> Layer: + """Parse a Layer element.""" + return Layer( + name=elem.get("name", ""), + is_visible=elem.get("is_visible", "true") == "true", + ) + + def _parse_property(self, elem: ET.Element) -> Property: + """Parse a Property element.""" + return Property( + name=elem.get("name", ""), + value=elem.get("value", ""), + ) diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..8581d80 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,11 @@ +SRC_DIR = ../../ +PYTHON ?= python3 + +TESTS = \ + test_ivreader.py + +.PHONY: \ + check + +check: ${TESTS} + PYTHONPATH=${SRC_DIR} ${PYTHON} -m pytest ${TESTS} -vv \ No newline at end of file diff --git a/tests/test_ivreader.py b/tests/test_ivreader.py new file mode 100644 index 0000000..2b759b7 --- /dev/null +++ b/tests/test_ivreader.py @@ -0,0 +1,251 @@ +""" +Tests for IVReader class +""" + +import pytest +from pathlib import Path +from templateprocessor.ivreader import IVReader +from templateprocessor.iv import ( + InterfaceView, + Function, + Language, + Encoding, + InterfaceKind, +) + + +class TestIVReader: + """Test cases for IVReader class.""" + + # Assuming the data directory is at the workspace root + @staticmethod + def get_test_data_file(filename: str) -> Path: + """Get the path to a test data file.""" + return Path(__file__).parent.parent / "data" / filename + + def test_read_simple_iv_xml(self): + """Test reading the simple.iv.xml file.""" + # Prepare + reader = IVReader() + iv_file = self.get_test_data_file("simple.iv.xml") + assert iv_file.exists() + + # Read + iv = reader.read(iv_file) + + # Verify basic attributes + assert isinstance(iv, InterfaceView) + assert iv.version == "1.3" + assert iv.asn1file == "simple.acn" + assert iv.UiFile == "interfaceview.ui.xml" + + # Verify functions were parsed + assert len(iv.functions) > 0 + + # Check specific function names + function_names = [f.name for f in iv.functions] + assert "host" in function_names + assert "master" in function_names + assert "slave" in function_names + assert "Worker" in function_names + assert "WorkerType" in function_names + + def test_read_nested_functions(self): + """Test reading nested functions in the simple.iv.xml file.""" + # Prepare + reader = IVReader() + iv_file = self.get_test_data_file("simple.iv.xml") + assert iv_file.exists() + + # Read + iv = reader.read(iv_file) + + # Verify basic attributes + assert isinstance(iv, InterfaceView) + assert iv.version == "1.3" + assert iv.asn1file == "simple.acn" + assert iv.UiFile == "interfaceview.ui.xml" + + # Verify functions were parsed + assert len(iv.functions) > 0 + + # Check specific function names + host = next((f for f in iv.functions if f.name == "host"), None) + function_names = [f.name for f in host.nested_functions] + assert "child1" in function_names + assert "child2" in function_names + + def test_read_functions_details(self): + """Test that function details are correctly parsed.""" + # Prepare + reader = IVReader() + iv_file = self.get_test_data_file("simple.iv.xml") + assert iv_file.exists() + + # Read + iv = reader.read(iv_file) + + # Find the 'host' function + host = next((f for f in iv.functions if f.name == "host"), None) + assert host is not None + assert host.language == Language.SDL + assert host.is_type == False + + # Check interfaces + assert len(host.provided_interfaces) > 0 + assert len(host.required_interfaces) > 0 + + # Check specific provided interface + child1_pi = next( + (pi for pi in host.provided_interfaces if pi.name == "child1_if"), None + ) + assert child1_pi is not None + assert child1_pi.kind == InterfaceKind.SPORADIC + assert len(child1_pi.input_parameters) == 1 + assert child1_pi.input_parameters[0].name == "p1" + assert child1_pi.input_parameters[0].type == "T-Int32" + assert child1_pi.input_parameters[0].encoding == Encoding.NATIVE + + def test_read_connections(self): + """Test that connections are correctly parsed.""" + # Prepare + reader = IVReader() + iv_file = self.get_test_data_file("simple.iv.xml") + assert iv_file.exists() + + # Read + iv = reader.read(iv_file) + + # Verify connections were parsed + assert len(iv.connections) > 0 + + # Check a specific connection + connection = next( + ( + c + for c in iv.connections + if c.id == "{0ed63357-7a9b-40f0-8392-27844d53ef6b}" + ), + None, + ) + assert connection.source is not None + assert connection.target is not None + assert connection.source.function_name == "master" + assert connection.target.function_name == "host" + + def test_read_worker_type_function(self): + """Test parsing function type and instance.""" + # Prepare + reader = IVReader() + iv_file = self.get_test_data_file("simple.iv.xml") + assert iv_file.exists() + + # Read + iv = reader.read(iv_file) + + # Find WorkerType (function type) + worker_type = next((f for f in iv.functions if f.name == "WorkerType"), None) + assert worker_type is not None + assert worker_type.is_type == True + assert worker_type.type_language == Language.SDL + + # Find Worker (instance of WorkerType) + worker = next((f for f in iv.functions if f.name == "Worker"), None) + assert worker is not None + assert worker.instance_of == "WorkerType" + assert worker.is_type == False + + def test_read_interface_with_multiple_parameters(self): + """Test parsing interface with multiple input parameters.""" + # Prepare + reader = IVReader() + iv_file = self.get_test_data_file("simple.iv.xml") + assert iv_file.exists() + + # Read + iv = reader.read(iv_file) + + # Find master function + master = next((f for f in iv.functions if f.name == "master"), None) + assert master is not None + + # Find unprotected RI with multiple parameters + unprotected_ri = next( + (ri for ri in master.required_interfaces if ri.name == "unprotected"), None + ) + assert unprotected_ri is not None + assert len(unprotected_ri.input_parameters) == 3 + + # Verify different encodings + unprotected_ri.input_parameters[0].name == "p1" + unprotected_ri.input_parameters[0].type == "T-Int32" + unprotected_ri.input_parameters[0].encoding == Encoding.NATIVE + unprotected_ri.input_parameters[1].name == "p2" + unprotected_ri.input_parameters[1].type == "T-Int32" + unprotected_ri.input_parameters[1].encoding == Encoding.UPER + unprotected_ri.input_parameters[2].name == "p3" + unprotected_ri.input_parameters[2].type == "T-Int32" + unprotected_ri.input_parameters[2].encoding == Encoding.ACN + + def test_read_output_parameters(self): + """Test parsing interface with output parameters.""" + # Prepare + reader = IVReader() + iv_file = self.get_test_data_file("simple.iv.xml") + assert iv_file.exists() + + # Read + iv = reader.read(iv_file) + + # Find slave function + slave = next((f for f in iv.functions if f.name == "slave"), None) + assert slave is not None + + # Find protected_if PI with output parameter + protected_pi = next( + (pi for pi in slave.provided_interfaces if pi.name == "protected_if"), None + ) + assert protected_pi is not None + assert len(protected_pi.input_parameters) == 1 + assert len(protected_pi.output_parameters) == 1 + assert protected_pi.output_parameters[0].name == "p2" + assert protected_pi.output_parameters[0].encoding == Encoding.UPER + + def test_read_layers_and_comments(self): + """Test parsing layers and comments.""" + # Prepare + reader = IVReader() + iv_file = self.get_test_data_file("simple.iv.xml") + assert iv_file.exists() + + # Read + iv = reader.read(iv_file) + + # Verify layers + assert len(iv.layers) > 0 + default_layer = next((l for l in iv.layers if l.name == "default"), None) + assert default_layer is not None + assert default_layer.is_visible == True + + # Verify comments + assert len(iv.comments) > 0 + + def test_read_string(self): + """Test reading from XML string.""" + reader = IVReader() + + xml_content = """ + + + + +""" + + iv = reader.read_string(xml_content) + + assert iv.version == "1.0" + assert iv.asn1file == "test.acn" + assert len(iv.functions) == 1 + assert iv.functions[0].name == "test_func" + assert len(iv.layers) == 1 From 14d8ea6883d35bf1269f2b1d0d3093351891ba69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Wed, 3 Dec 2025 20:59:21 +0100 Subject: [PATCH 04/21] Added GitHub workflow --- .github/workflows/workflow.yml | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/workflow.yml diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..8b4cda9 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,38 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Workflow + +on: [pull_request] + +permissions: + contents: read + +jobs: + check: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Functional check + run: make check + + - name: Style check + run: make check-format + From 4f58b27f7b91ed708c043f76f1c3b2ff5c51536a Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:10:44 +0100 Subject: [PATCH 05/21] Update Makefile Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2114965..640171b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ BLACK=black -PYTHON= ?= python3 +PYTHON ?= python3 .PHONY : \ check \ From cce1f0a799a04875ca5c15058f127b86d3d00910 Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:11:17 +0100 Subject: [PATCH 06/21] Update tests/test_ivreader.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_ivreader.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_ivreader.py b/tests/test_ivreader.py index 2b759b7..d125ce2 100644 --- a/tests/test_ivreader.py +++ b/tests/test_ivreader.py @@ -177,15 +177,15 @@ def test_read_interface_with_multiple_parameters(self): assert len(unprotected_ri.input_parameters) == 3 # Verify different encodings - unprotected_ri.input_parameters[0].name == "p1" - unprotected_ri.input_parameters[0].type == "T-Int32" - unprotected_ri.input_parameters[0].encoding == Encoding.NATIVE - unprotected_ri.input_parameters[1].name == "p2" - unprotected_ri.input_parameters[1].type == "T-Int32" - unprotected_ri.input_parameters[1].encoding == Encoding.UPER - unprotected_ri.input_parameters[2].name == "p3" - unprotected_ri.input_parameters[2].type == "T-Int32" - unprotected_ri.input_parameters[2].encoding == Encoding.ACN + assert unprotected_ri.input_parameters[0].name == "p1" + assert unprotected_ri.input_parameters[0].type == "T-Int32" + assert unprotected_ri.input_parameters[0].encoding == Encoding.NATIVE + assert unprotected_ri.input_parameters[1].name == "p2" + assert unprotected_ri.input_parameters[1].type == "T-Int32" + assert unprotected_ri.input_parameters[1].encoding == Encoding.UPER + assert unprotected_ri.input_parameters[2].name == "p3" + assert unprotected_ri.input_parameters[2].type == "T-Int32" + assert unprotected_ri.input_parameters[2].encoding == Encoding.ACN def test_read_output_parameters(self): """Test parsing interface with output parameters.""" From 02b02e7a5394e0227499c5fb9133af8542ca5691 Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:11:40 +0100 Subject: [PATCH 07/21] Update templateprocessor/iv.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/iv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templateprocessor/iv.py b/templateprocessor/iv.py index 8f78508..726e524 100644 --- a/templateprocessor/iv.py +++ b/templateprocessor/iv.py @@ -113,7 +113,7 @@ class Implementation: """Function implementation details.""" name: str - language: Language + language: Optional[Language] = None @dataclass From 5de4d54e3aecd6b6dabaaad03ab895fa3926710c Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:12:51 +0100 Subject: [PATCH 08/21] Update templateprocessor/ivreader.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/ivreader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templateprocessor/ivreader.py b/templateprocessor/ivreader.py index a622d6d..03cad50 100644 --- a/templateprocessor/ivreader.py +++ b/templateprocessor/ivreader.py @@ -7,7 +7,7 @@ import xml.etree.ElementTree as ET from pathlib import Path -from typing import Union, Optional +from typing import Union from templateprocessor.iv import ( InterfaceView, From ec86da21859feb96d91f67bfd06d9f73b951605e Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:13:11 +0100 Subject: [PATCH 09/21] Update templateprocessor/ivreader.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/ivreader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templateprocessor/ivreader.py b/templateprocessor/ivreader.py index 03cad50..9d07047 100644 --- a/templateprocessor/ivreader.py +++ b/templateprocessor/ivreader.py @@ -277,7 +277,7 @@ def _parse_comment(self, elem: ET.Element) -> Comment: return Comment( id=elem.get("id", ""), name=elem.get("name", ""), - required_system_element=elem.get("required_system_element", "NO"), + required_system_element=elem.get("required_system_element", "NO") == "YES", ) def _parse_layer(self, elem: ET.Element) -> Layer: From 64ecb389d6ac788d7f991c6c3faebb9fe71b39f3 Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:13:24 +0100 Subject: [PATCH 10/21] Update templateprocessor/iv.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/iv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templateprocessor/iv.py b/templateprocessor/iv.py index 726e524..058e51b 100644 --- a/templateprocessor/iv.py +++ b/templateprocessor/iv.py @@ -7,7 +7,7 @@ """ from dataclasses import dataclass, field, fields -from typing import List, Optional, Literal +from typing import List, Optional from enum import Enum From 15362580f33c7d87a54f790e0af4c7d512b07e96 Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:14:06 +0100 Subject: [PATCH 11/21] Update templateprocessor/iv.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/iv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templateprocessor/iv.py b/templateprocessor/iv.py index 058e51b..4ad1286 100644 --- a/templateprocessor/iv.py +++ b/templateprocessor/iv.py @@ -202,7 +202,7 @@ class InterfaceView: version: str asn1file: str - UiFile: str + uiFile: str modifierHash: str functions: List[Function] = field(default_factory=list) connections: List[Connection] = field(default_factory=list) From a939966c7af41cb060243d43bd15c35c5ad7b76a Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:14:17 +0100 Subject: [PATCH 12/21] Update templateprocessor/iv.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/iv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templateprocessor/iv.py b/templateprocessor/iv.py index 4ad1286..c3e6754 100644 --- a/templateprocessor/iv.py +++ b/templateprocessor/iv.py @@ -6,7 +6,7 @@ generation of IV data. """ -from dataclasses import dataclass, field, fields +from dataclasses import dataclass, field from typing import List, Optional from enum import Enum From 40609bbb900a6b9c7a951288dace783ee5301e4d Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:14:38 +0100 Subject: [PATCH 13/21] Update templateprocessor/ivreader.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/ivreader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templateprocessor/ivreader.py b/templateprocessor/ivreader.py index 9d07047..142c48e 100644 --- a/templateprocessor/ivreader.py +++ b/templateprocessor/ivreader.py @@ -237,7 +237,7 @@ def _parse_implementation(self, elem: ET.Element) -> Implementation: """Parse an Implementation element.""" return Implementation( name=elem.get("name", ""), - language=elem.get("language", ""), + language=Language(elem.get("language", "")), ) def _parse_connection(self, elem: ET.Element) -> Connection: From 38a02bb3ad6f6d520d127da8e0ecbba07909da3d Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:14:55 +0100 Subject: [PATCH 14/21] Update templateprocessor/iv.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/iv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templateprocessor/iv.py b/templateprocessor/iv.py index c3e6754..84c4025 100644 --- a/templateprocessor/iv.py +++ b/templateprocessor/iv.py @@ -123,7 +123,7 @@ class Function: id: str name: str is_type: bool - language: Language + language: Optional[Language] = None default_implementation: str = "default" fixed_system_element: bool = False required_system_element: bool = False From 89a12c71b049a4d41232081718f83a479ac60c1a Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:15:10 +0100 Subject: [PATCH 15/21] Update templateprocessor/ivreader.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/ivreader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templateprocessor/ivreader.py b/templateprocessor/ivreader.py index 142c48e..4a0fb08 100644 --- a/templateprocessor/ivreader.py +++ b/templateprocessor/ivreader.py @@ -244,7 +244,7 @@ def _parse_connection(self, elem: ET.Element) -> Connection: """Parse a Connection element.""" connection = Connection( id=elem.get("id", ""), - required_system_element=elem.get("required_system_element", "NO"), + required_system_element=elem.get("required_system_element", "NO") == "YES", name=elem.get("name"), ) From 02e5c5562b1f836c902956c3eeaa706d46de6876 Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:15:48 +0100 Subject: [PATCH 16/21] Update templateprocessor/iv.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- templateprocessor/iv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templateprocessor/iv.py b/templateprocessor/iv.py index 84c4025..734e9a1 100644 --- a/templateprocessor/iv.py +++ b/templateprocessor/iv.py @@ -169,9 +169,9 @@ class Connection: id: str required_system_element: bool = False - name: str = None - source: ConnectionSource = None - target: ConnectionTarget = None + name: Optional[str] = None + source: Optional[ConnectionSource] = None + target: Optional[ConnectionTarget] = None @dataclass From a9f2bd01b88844f45210271e5980203224894b52 Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:16:14 +0100 Subject: [PATCH 17/21] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b202f28..6091afe 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Template Processor (TP), created as a part of \"Model-Based Execution Platform for Space Applications\" project (contract 4000146882/24/NL/KK) financed by the European Space Agency. -TP is a template processing engine developed for TASTE Document Generator. It main function is to consume the provided inputs (e.g., TASTE Interface View data), instantiate templates and translate them into format that can be integrated deliverable documents. Base requirements are provided in MBEP-N7S-EP-SRS, while the overall design is documented in MBEP-N7S-EP-SDD. +TP is a template processing engine developed for TASTE Document Generator. Its main function is to consume the provided inputs (e.g., TASTE Interface View data), instantiate templates and translate them into format that can be integrated deliverable documents. Base requirements are provided in MBEP-N7S-EP-SRS, while the overall design is documented in MBEP-N7S-EP-SDD. ## Installation From 918be46484ca49eff00b39857b817089ce73bfd5 Mon Sep 17 00:00:00 2001 From: Lurkerpas Date: Wed, 3 Dec 2025 21:16:34 +0100 Subject: [PATCH 18/21] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6091afe..37c4e42 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## General -Template Processor (TP), created as a part of \"Model-Based Execution Platform for Space Applications\" project (contract 4000146882/24/NL/KK) financed by the European Space Agency. +Template Processor (TP), created as a part of "Model-Based Execution Platform for Space Applications" project (contract 4000146882/24/NL/KK) financed by the European Space Agency. TP is a template processing engine developed for TASTE Document Generator. Its main function is to consume the provided inputs (e.g., TASTE Interface View data), instantiate templates and translate them into format that can be integrated deliverable documents. Base requirements are provided in MBEP-N7S-EP-SRS, while the overall design is documented in MBEP-N7S-EP-SDD. From cde7d7df8263b649821fd85ee652c15ee8ae7836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Wed, 3 Dec 2025 21:21:13 +0100 Subject: [PATCH 19/21] Fixed automated fixes --- templateprocessor/ivreader.py | 2 +- tests/test_ivreader.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templateprocessor/ivreader.py b/templateprocessor/ivreader.py index 4a0fb08..2c16eea 100644 --- a/templateprocessor/ivreader.py +++ b/templateprocessor/ivreader.py @@ -87,7 +87,7 @@ def _parse_interface_view(self, root: ET.Element) -> InterfaceView: iv = InterfaceView( version=root.get("version", ""), asn1file=root.get("asn1file", ""), - UiFile=root.get("UiFile", ""), + uiFile=root.get("UiFile", ""), modifierHash=root.get("modifierHash", ""), ) diff --git a/tests/test_ivreader.py b/tests/test_ivreader.py index d125ce2..4377a16 100644 --- a/tests/test_ivreader.py +++ b/tests/test_ivreader.py @@ -37,7 +37,7 @@ def test_read_simple_iv_xml(self): assert isinstance(iv, InterfaceView) assert iv.version == "1.3" assert iv.asn1file == "simple.acn" - assert iv.UiFile == "interfaceview.ui.xml" + assert iv.uiFile == "interfaceview.ui.xml" # Verify functions were parsed assert len(iv.functions) > 0 @@ -64,7 +64,7 @@ def test_read_nested_functions(self): assert isinstance(iv, InterfaceView) assert iv.version == "1.3" assert iv.asn1file == "simple.acn" - assert iv.UiFile == "interfaceview.ui.xml" + assert iv.uiFile == "interfaceview.ui.xml" # Verify functions were parsed assert len(iv.functions) > 0 From de679a722a7b7b05b51aaf4ad0bdada89b391a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Wed, 3 Dec 2025 21:22:54 +0100 Subject: [PATCH 20/21] Updated requirements --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0990c86..0c82c2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ # Core dependencies for Template Processor # Template processing engine for TASTE Document Generator - +pytest==7.4.2 +black==24.3.0 +mako==1.3.10 From 25c3b56814b10a226a7c6dfceed13def04ee447e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurowski?= Date: Wed, 3 Dec 2025 21:25:01 +0100 Subject: [PATCH 21/21] Reformatted after updating black --- templateprocessor/ivreader.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/templateprocessor/ivreader.py b/templateprocessor/ivreader.py index 2c16eea..493cda7 100644 --- a/templateprocessor/ivreader.py +++ b/templateprocessor/ivreader.py @@ -119,9 +119,9 @@ def _parse_function(self, elem: ET.Element) -> Function: id=elem.get("id", ""), name=elem.get("name", ""), is_type=elem.get("is_type", "NO") == "YES", - language=Language(elem.get("language", "")) - if elem.get("language") - else None, + language=( + Language(elem.get("language", "")) if elem.get("language") else None + ), default_implementation=elem.get("default_implementation", "default"), fixed_system_element=elem.get("fixed_system_element", "NO") == "YES", required_system_element=elem.get("required_system_element", "NO") == "YES", @@ -129,9 +129,11 @@ def _parse_function(self, elem: ET.Element) -> Function: instances_max=int(elem.get("instances_max", "1")), startup_priority=int(elem.get("startup_priority", "1")), instance_of=elem.get("instance_of"), - type_language=Language(elem.get("type_language")) - if elem.get("type_language") - else None, + type_language=( + Language(elem.get("type_language")) + if elem.get("type_language") + else None + ), ) # Parse properties @@ -184,9 +186,11 @@ def _parse_interface(self, elem: ET.Element) -> FunctionInterface: queue_size=int(elem.get("queue_size")) if elem.get("queue_size") else None, miat=int(elem.get("miat")) if elem.get("miat") else None, period=int(elem.get("period")) if elem.get("period") else None, - dispatch_offset=int(elem.get("dispatch_offset")) - if elem.get("dispatch_offset") - else None, + dispatch_offset=( + int(elem.get("dispatch_offset")) + if elem.get("dispatch_offset") + else None + ), priority=int(elem.get("priority")) if elem.get("priority") else None, )