harmonized-telemetry-format 0.0.2a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: harmonized-telemetry-format
3
+ Version: 0.0.2a1
4
+ Summary: Core library for reading and writing the Harmonized Telemetry Format (HTF).
5
+ Author-email: Max Schlosser <schlosse@hs-mittweida.de>
6
+ Project-URL: Homepage, https://github.com/Schloool/harmonized-telemetry-format
7
+ Project-URL: Bug Tracker, https://github.com/Schloool/harmonized-telemetry-format/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
12
+ Classifier: Topic :: Utilities
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Harmonized Telemetry Format (HTF)
17
+
18
+ Detailed description coming soon.
19
+
20
+ Exemplary HTF file content:
21
+ ```htf
22
+ [static_metadata;s]12.51
23
+ [dim_metadata;index;delta]2;0.08;4;0.484
24
+ (channel;s;50;135)0=12.27;1=12.42;...
25
+ (empty_channel;m;50;135)0=;
26
+ ```
@@ -0,0 +1,11 @@
1
+ # Harmonized Telemetry Format (HTF)
2
+
3
+ Detailed description coming soon.
4
+
5
+ Exemplary HTF file content:
6
+ ```htf
7
+ [static_metadata;s]12.51
8
+ [dim_metadata;index;delta]2;0.08;4;0.484
9
+ (channel;s;50;135)0=12.27;1=12.42;...
10
+ (empty_channel;m;50;135)0=;
11
+ ```
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: harmonized-telemetry-format
3
+ Version: 0.0.2a1
4
+ Summary: Core library for reading and writing the Harmonized Telemetry Format (HTF).
5
+ Author-email: Max Schlosser <schlosse@hs-mittweida.de>
6
+ Project-URL: Homepage, https://github.com/Schloool/harmonized-telemetry-format
7
+ Project-URL: Bug Tracker, https://github.com/Schloool/harmonized-telemetry-format/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
12
+ Classifier: Topic :: Utilities
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Harmonized Telemetry Format (HTF)
17
+
18
+ Detailed description coming soon.
19
+
20
+ Exemplary HTF file content:
21
+ ```htf
22
+ [static_metadata;s]12.51
23
+ [dim_metadata;index;delta]2;0.08;4;0.484
24
+ (channel;s;50;135)0=12.27;1=12.42;...
25
+ (empty_channel;m;50;135)0=;
26
+ ```
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ harmonized_telemetry_format.egg-info/PKG-INFO
4
+ harmonized_telemetry_format.egg-info/SOURCES.txt
5
+ harmonized_telemetry_format.egg-info/dependency_links.txt
6
+ harmonized_telemetry_format.egg-info/top_level.txt
7
+ htf_core/__init__.py
8
+ htf_core/models.py
9
+ htf_core/reader.py
10
+ htf_core/writer.py
11
+ tests/test_reader.py
12
+ tests/test_writer.py
@@ -0,0 +1,39 @@
1
+ from typing import Optional
2
+
3
+
4
+ class HarmonizedTelemetryChannel:
5
+ """A telemetry channel used within a recording."""
6
+
7
+ def __init__(self, name: str, unit: Optional[str], frequency: Optional[int],
8
+ total_values: int, values: list[tuple[int, object]]):
9
+ self.name = name
10
+ self.unit = unit
11
+ self.frequency = frequency
12
+ self.total_values = total_values
13
+ self.values = values
14
+
15
+
16
+ class HarmonizedMetadataEntry:
17
+ """
18
+ Unified class for both static metadata (single record, n=1) and dimensional aggregations (n>=1).
19
+ Maps directly to the HTF line structure: [name;property_1;property_2;...]value1_1;value1_2;value2_1;value2_2;...
20
+ """
21
+
22
+ # TODO: Values are wrong
23
+ def __init__(self, name: str, column_names: list[str], column_values: dict[str, list[object]]):
24
+ self.name = name
25
+ self.column_names = column_names
26
+ self.column_values = column_values
27
+ self._n = len(column_names)
28
+
29
+ @property
30
+ def is_static(self) -> bool:
31
+ """Helper to quickly identify single-record static metadata."""
32
+ return self._n == 1 and len(self.column_values) == 1
33
+
34
+
35
+ class HarmonizedTelemetryRecording:
36
+ """Representation of a complete telemetry recording with metadata and channels."""
37
+ def __init__(self, metadata: Optional[list[HarmonizedMetadataEntry]], channels: list[HarmonizedTelemetryChannel]):
38
+ self.metadata = metadata
39
+ self.channels = channels
@@ -0,0 +1,115 @@
1
+ import ast
2
+ import re
3
+ from io import TextIOWrapper
4
+
5
+ from htf_core.models import HarmonizedTelemetryRecording, HarmonizedMetadataEntry, HarmonizedTelemetryChannel
6
+
7
+
8
+ METADATA_REGEX = re.compile(
9
+ r"^\[(?P<preamble_content>[^]]+)]"
10
+ r"(?P<data_content>.*)$"
11
+ )
12
+
13
+ CHANNEL_REGEX = re.compile(
14
+ r"^\("
15
+ r"(?P<name>[^;]+);"
16
+ r"(?P<unit>[^;]+);"
17
+ r"(?P<frequency>[^;]*);"
18
+ r"(?P<value_count>[^)]+)\)"
19
+ r"(?P<data_content>.*)$"
20
+ )
21
+
22
+
23
+ class HtfReader:
24
+ def __init__(self, entries: list[str]):
25
+ self.entries = entries
26
+
27
+ @classmethod
28
+ def from_str(cls, text: str):
29
+ content = text.split("\n")
30
+ return cls(content)
31
+
32
+ @classmethod
33
+ def from_file(cls, file: TextIOWrapper):
34
+ content = file.readlines()
35
+ return cls(content)
36
+
37
+ def read(self) -> HarmonizedTelemetryRecording:
38
+ metadata_entries = []
39
+ telemetry_channels = []
40
+ for entry in self.entries:
41
+ metadata_match = METADATA_REGEX.match(entry)
42
+ if metadata_match:
43
+ metadata_entries.append(self.read_metadata_entry(entry))
44
+ continue
45
+
46
+ channel_match = CHANNEL_REGEX.match(entry)
47
+ if channel_match:
48
+ telemetry_channels.append(self.read_telemetry_channel(entry))
49
+ continue
50
+
51
+ raise ValueError(f"Entry does not match metadata or channel format: {entry}")
52
+ return HarmonizedTelemetryRecording(
53
+ metadata=metadata_entries if len(metadata_entries) > 0 else None,
54
+ channels=telemetry_channels
55
+ )
56
+
57
+ @staticmethod
58
+ def read_metadata_entry(line: str) -> HarmonizedMetadataEntry:
59
+ match = METADATA_REGEX.match(line)
60
+ if not match:
61
+ raise ValueError(f"Line does not match metadata format: {line}")
62
+
63
+ preamble_content = match.group("preamble_content")
64
+ data_content = match.group("data_content")
65
+
66
+ parts = preamble_content.split(";")
67
+ name = parts[0]
68
+ column_names = parts[1:]
69
+
70
+ column_values = {col_name: [] for col_name in column_names}
71
+ data_values = data_content.split(";")
72
+ for i, value in enumerate(data_values):
73
+ col_name = column_names[i % len(column_names)]
74
+ column_values[col_name].append(value)
75
+
76
+ return HarmonizedMetadataEntry(
77
+ name=name,
78
+ column_names=column_names,
79
+ column_values=column_values,
80
+ )
81
+
82
+ @staticmethod
83
+ def read_telemetry_channel(line: str) -> HarmonizedTelemetryChannel:
84
+ match = CHANNEL_REGEX.match(line)
85
+ if not match:
86
+ raise ValueError(f"Line does not match channel format: {line}")
87
+
88
+ name = match.group("name")
89
+ unit = match.group("unit")
90
+ frequency_str = match.group("frequency")
91
+ frequency = int(frequency_str) if frequency_str else None
92
+ total_values = int(match.group("value_count"))
93
+ data_content = match.group("data_content")
94
+
95
+ values = []
96
+ if data_content:
97
+ value_pairs = data_content.split(";")
98
+ for pair in value_pairs:
99
+ index_str, value_str = pair.split("=", 1)
100
+ index = int(index_str)
101
+ value = ast.literal_eval(value_str) if value_str else None
102
+
103
+ # Omit duplicate consecutive values
104
+ if index > 0 and value == values[-1][1]:
105
+ continue
106
+
107
+ values.append((index, value))
108
+
109
+ return HarmonizedTelemetryChannel(
110
+ name=name,
111
+ unit=unit,
112
+ frequency=frequency,
113
+ total_values=total_values,
114
+ values=values
115
+ )
@@ -0,0 +1,58 @@
1
+ from htf_core.models import HarmonizedTelemetryRecording, HarmonizedMetadataEntry, HarmonizedTelemetryChannel
2
+
3
+
4
+ class HtfWriter:
5
+ def __init__(self, recording: HarmonizedTelemetryRecording):
6
+ self.recording = recording
7
+
8
+ def serialize(self) -> str:
9
+ lines = []
10
+ if self.recording.metadata:
11
+ for entry in self.recording.metadata:
12
+ lines.append(self.compose_metadata_entry(entry))
13
+
14
+ for channel in self.recording.channels:
15
+ lines.append(self.compose_channel(channel))
16
+
17
+ return "\n".join(lines)
18
+
19
+ @staticmethod
20
+ def compose_metadata_entry(entry: HarmonizedMetadataEntry) -> str:
21
+ headers = ";".join(entry.column_names)
22
+ preamble = f"[{entry.name};{headers}]"
23
+
24
+ if not entry.column_names:
25
+ raise "No column names provided"
26
+
27
+ first_column_key = entry.column_names[0]
28
+ num_rows = len(entry.column_values.get(first_column_key, []))
29
+
30
+ row_data_list = []
31
+
32
+ for data_index in range(num_rows):
33
+ current_row_values = []
34
+
35
+ for col_name in entry.column_names:
36
+ column_list = entry.column_values.get(col_name, [])
37
+
38
+ if data_index < len(column_list):
39
+ current_row_values.append(str(column_list[data_index]))
40
+
41
+ row_data_list.append(";".join(current_row_values))
42
+
43
+ data = ";".join(row_data_list)
44
+
45
+ return f"{preamble}{data}"
46
+
47
+ @staticmethod
48
+ def compose_channel(channel: HarmonizedTelemetryChannel) -> str:
49
+ frequency = channel.frequency if channel.frequency is not None else ""
50
+ preamble = f"({channel.name};{channel.unit};{frequency};{channel.total_values})"
51
+
52
+ # Create index-value pairs, omitting repeating values
53
+ value_pairs = [f"{index}={value}"
54
+ for index, value in channel.values
55
+ if index == 0 or value != channel.values[index - 1]]
56
+
57
+ values = ";".join(value_pairs)
58
+ return f"{preamble}{values}"
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "harmonized-telemetry-format"
7
+ version = "0.0.2a1"
8
+ authors = [
9
+ { name="Max Schlosser", email="schlosse@hs-mittweida.de" },
10
+ ]
11
+ description = "Core library for reading and writing the Harmonized Telemetry Format (HTF)."
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ dependencies = []
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Scientific/Engineering :: Information Analysis",
20
+ "Topic :: Utilities",
21
+ ]
22
+
23
+ [project.urls]
24
+ "Homepage" = "https://github.com/Schloool/harmonized-telemetry-format"
25
+ "Bug Tracker" = "https://github.com/Schloool/harmonized-telemetry-format/issues"
26
+
27
+ [tool.setuptools]
28
+ packages = ["htf_core"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,83 @@
1
+ from unittest import TestCase
2
+
3
+ from htf_core.reader import HtfReader
4
+
5
+
6
+ class Test(TestCase):
7
+ def test_reads_complete_recording(self):
8
+ entries = [
9
+ "[Metadata;Property1;Property2]Value1;Value2",
10
+ "(ChannelName;Unit;10;3)0=100;1=200;2=300"
11
+ ]
12
+ reader = HtfReader(entries=entries)
13
+
14
+ recording = reader.read()
15
+
16
+ self.assertIsNotNone(recording.metadata)
17
+ self.assertEqual(len(recording.metadata), 1)
18
+ self.assertEqual(len(recording.channels), 1)
19
+
20
+ def test_parses_valid_metadata(self):
21
+ valid_metadata_line = "[Metadata;Property1;Property2]Value1;Value2"
22
+ reader = HtfReader(entries=[valid_metadata_line])
23
+
24
+ metadata_entry = reader.read_metadata_entry(valid_metadata_line)
25
+
26
+ self.assertEqual(metadata_entry.name, "Metadata")
27
+ self.assertEqual(metadata_entry.column_names, ["Property1", "Property2"])
28
+ self.assertEqual(metadata_entry.column_values, {
29
+ "Property1": ["Value1"],
30
+ "Property2": ["Value2"]
31
+ })
32
+
33
+ def test_parses_valid_channel(self):
34
+ valid_channel_line = "(ChannelName;Unit;10;5)0=100;1=200;2=300"
35
+ reader = HtfReader(entries=[valid_channel_line])
36
+
37
+ channel = reader.read_telemetry_channel(valid_channel_line)
38
+
39
+ self.assertEqual(channel.name, "ChannelName")
40
+ self.assertEqual(channel.unit, "Unit")
41
+ self.assertEqual(channel.frequency, 10)
42
+ self.assertEqual(channel.total_values, 5)
43
+ self.assertEqual(channel.values, [(0, 100), (1, 200), (2, 300)])
44
+
45
+ def test_emits_duplicate_channel_values(self):
46
+ valid_channel_line = "(ChannelName;Unit;;5)0=100;1=100;2=200;3=200;4=300"
47
+ reader = HtfReader(entries=[valid_channel_line])
48
+
49
+ channel = reader.read_telemetry_channel(valid_channel_line)
50
+
51
+ self.assertIsNone(channel.frequency)
52
+ self.assertEqual(channel.total_values, 5)
53
+ self.assertEqual(channel.values, [(0, 100), (2, 200), (4, 300)])
54
+
55
+ def test_sets_none_values_for_missing_frequency(self):
56
+ channel_line = "(ChannelName;Unit;;3)0=100;1=200;2=300"
57
+ reader = HtfReader(entries=[channel_line])
58
+
59
+ channel = reader.read_telemetry_channel(channel_line)
60
+
61
+ self.assertIsNone(channel.frequency)
62
+
63
+ def test_sets_none_values_for_missing_channel_values(self):
64
+ channel_line = "(ChannelName;Unit;;3)0="
65
+ reader = HtfReader(entries=[channel_line])
66
+
67
+ channel = reader.read_telemetry_channel(channel_line)
68
+
69
+ self.assertEqual(channel.values, [(0, None)])
70
+
71
+ def test_throws_on_invalid_metadata(self):
72
+ invalid_metadata_line = "[InvalidMetadata;Data;Data"
73
+ reader = HtfReader(entries=[invalid_metadata_line])
74
+
75
+ with self.assertRaises(ValueError):
76
+ reader.read_metadata_entry(invalid_metadata_line)
77
+
78
+ def test_throws_on_invalid_channel(self):
79
+ invalid_channel_line = "(InvalidChannel;Unit;Frequency;TotalValues"
80
+ reader = HtfReader(entries=[invalid_channel_line])
81
+
82
+ with self.assertRaises(ValueError):
83
+ reader.read_telemetry_channel(invalid_channel_line)
@@ -0,0 +1,31 @@
1
+ from unittest import TestCase
2
+
3
+ from htf_core.models import HarmonizedMetadataEntry
4
+ from htf_core.writer import HtfWriter
5
+
6
+
7
+ class Test(TestCase):
8
+ def test_compose_static_metadata_entry(self):
9
+ entry = HarmonizedMetadataEntry(
10
+ name="Static",
11
+ column_names=["Property1"],
12
+ column_values={"Property1": [1]}
13
+ )
14
+
15
+ text = HtfWriter.compose_metadata_entry(entry)
16
+
17
+ self.assertEqual(text, "[Static;Property1]1")
18
+
19
+ def test_compose_dimensional_metadata_entry(self):
20
+ entry = HarmonizedMetadataEntry(
21
+ name="Dimensions",
22
+ column_names=["Dim1", "Dim2"],
23
+ column_values={
24
+ "Dim1": [10, 20],
25
+ "Dim2": ["A", "B"]
26
+ }
27
+ )
28
+
29
+ text = HtfWriter.compose_metadata_entry(entry)
30
+
31
+ self.assertEqual(text, "[Dimensions;Dim1;Dim2]10;A;20;B")