From aba01af7322e474cbf432552f213c6ce456754de Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Thu, 12 Mar 2026 10:39:03 -0700 Subject: [PATCH 1/4] feat: add mapache-py, Python port of mapache-go telemetry library --- mapache-py/.github/workflows/ci.yml | 51 ++++ mapache-py/.github/workflows/publish.yml | 24 ++ mapache-py/pyproject.toml | 40 +++ mapache-py/src/mapache/__init__.py | 46 +++ mapache-py/src/mapache/binary.py | 173 +++++++++++ mapache-py/src/mapache/message.py | 124 ++++++++ mapache-py/src/mapache/ping.py | 11 + mapache-py/src/mapache/py.typed | 0 mapache-py/src/mapache/signal.py | 16 + mapache-py/src/mapache/vehicle.py | 69 +++++ mapache-py/tests/__init__.py | 0 mapache-py/tests/test_binary.py | 362 +++++++++++++++++++++++ mapache-py/tests/test_message.py | 279 +++++++++++++++++ mapache-py/tests/test_vehicle.py | 57 ++++ 14 files changed, 1252 insertions(+) create mode 100644 mapache-py/.github/workflows/ci.yml create mode 100644 mapache-py/.github/workflows/publish.yml create mode 100644 mapache-py/pyproject.toml create mode 100644 mapache-py/src/mapache/__init__.py create mode 100644 mapache-py/src/mapache/binary.py create mode 100644 mapache-py/src/mapache/message.py create mode 100644 mapache-py/src/mapache/ping.py create mode 100644 mapache-py/src/mapache/py.typed create mode 100644 mapache-py/src/mapache/signal.py create mode 100644 mapache-py/src/mapache/vehicle.py create mode 100644 mapache-py/tests/__init__.py create mode 100644 mapache-py/tests/test_binary.py create mode 100644 mapache-py/tests/test_message.py create mode 100644 mapache-py/tests/test_vehicle.py diff --git a/mapache-py/.github/workflows/ci.yml b/mapache-py/.github/workflows/ci.yml new file mode 100644 index 00000000..02db8545 --- /dev/null +++ b/mapache-py/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: [main] + paths: + - "mapache-py/**" + pull_request: + branches: [main] + paths: + - "mapache-py/**" + +defaults: + run: + working-directory: mapache-py + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - run: pip install ruff + - run: ruff format --check src/ tests/ + - run: ruff check src/ tests/ + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - run: pip install -e ".[dev]" + - run: mypy src/ + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - run: pip install -e ".[dev]" + - run: pytest tests/ -v diff --git a/mapache-py/.github/workflows/publish.yml b/mapache-py/.github/workflows/publish.yml new file mode 100644 index 00000000..0b471a11 --- /dev/null +++ b/mapache-py/.github/workflows/publish.yml @@ -0,0 +1,24 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +defaults: + run: + working-directory: mapache-py + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - run: pip install build twine + - run: python -m build + - run: twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} diff --git a/mapache-py/pyproject.toml b/mapache-py/pyproject.toml new file mode 100644 index 00000000..f13319ea --- /dev/null +++ b/mapache-py/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "gr-mapache" +version = "0.1.0" +description = "Mapache telemetry library for Gaucho Racing" +license = "MIT" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "ruff", + "mypy", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/mapache"] + +[tool.ruff] +target-version = "py310" +line-length = 120 + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W", "UP"] + +[tool.mypy] +strict = true diff --git a/mapache-py/src/mapache/__init__.py b/mapache-py/src/mapache/__init__.py new file mode 100644 index 00000000..7e68434b --- /dev/null +++ b/mapache-py/src/mapache/__init__.py @@ -0,0 +1,46 @@ +from mapache.binary import ( + big_endian_bytes_to_signed_int, + big_endian_bytes_to_unsigned_int, + big_endian_signed_int_to_binary, + big_endian_signed_int_to_binary_string, + big_endian_unsigned_int_to_binary, + big_endian_unsigned_int_to_binary_string, + little_endian_bytes_to_signed_int, + little_endian_bytes_to_unsigned_int, + little_endian_signed_int_to_binary, + little_endian_signed_int_to_binary_string, + little_endian_unsigned_int_to_binary, + little_endian_unsigned_int_to_binary_string, +) +from mapache.message import Endian, ExportSignalFunc, Field, Message, SignMode, new_field +from mapache.ping import Ping +from mapache.signal import Signal +from mapache.vehicle import Marker, Segment, Session, Vehicle, derive_segments + +__all__ = [ + "big_endian_bytes_to_signed_int", + "big_endian_bytes_to_unsigned_int", + "big_endian_signed_int_to_binary", + "big_endian_signed_int_to_binary_string", + "big_endian_unsigned_int_to_binary", + "big_endian_unsigned_int_to_binary_string", + "derive_segments", + "little_endian_bytes_to_signed_int", + "little_endian_bytes_to_unsigned_int", + "little_endian_signed_int_to_binary", + "little_endian_signed_int_to_binary_string", + "little_endian_unsigned_int_to_binary", + "little_endian_unsigned_int_to_binary_string", + "new_field", + "Endian", + "ExportSignalFunc", + "Field", + "Marker", + "Message", + "Ping", + "Segment", + "Session", + "Signal", + "SignMode", + "Vehicle", +] diff --git a/mapache-py/src/mapache/binary.py b/mapache-py/src/mapache/binary.py new file mode 100644 index 00000000..a3214d59 --- /dev/null +++ b/mapache-py/src/mapache/binary.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import struct + + +def big_endian_unsigned_int_to_binary_string(num: int, num_bytes: int) -> str: + b = big_endian_unsigned_int_to_binary(num, num_bytes) + return "".join(f"{byte:08b}" for byte in b) + + +def big_endian_unsigned_int_to_binary(num: int, num_bytes: int) -> bytes: + if num < 0: + raise ValueError("cannot convert negative number to binary") + if num_bytes < 1: + raise ValueError("cannot convert to binary with less than 1 byte") + if num_bytes < 8 and num >= (1 << (num_bytes * 8)): + raise ValueError(f"number is too large to fit in {num_bytes} bytes") + + if num_bytes == 1: + return struct.pack(">B", num) + elif num_bytes == 2: + return struct.pack(">H", num) + elif num_bytes == 4: + return struct.pack(">I", num) + elif num_bytes == 8: + return struct.pack(">Q", num) + + result = bytearray(num_bytes) + for i in range(num_bytes): + result[i] = (num >> ((num_bytes - i - 1) * 8)) & 0xFF + return bytes(result) + + +def big_endian_signed_int_to_binary_string(num: int, num_bytes: int) -> str: + b = big_endian_signed_int_to_binary(num, num_bytes) + return "".join(f"{byte:08b}" for byte in b) + + +def big_endian_signed_int_to_binary(num: int, num_bytes: int) -> bytes: + if num_bytes < 1: + raise ValueError("cannot convert to binary with less than 1 byte") + min_value = -(1 << ((num_bytes * 8) - 1)) + max_value = (1 << ((num_bytes * 8) - 1)) - 1 + if num < min_value or num > max_value: + raise ValueError(f"number is too large to fit in {num_bytes} bytes") + + if num_bytes == 1: + return struct.pack(">b", num) + elif num_bytes == 2: + return struct.pack(">h", num) + elif num_bytes == 4: + return struct.pack(">i", num) + elif num_bytes == 8: + return struct.pack(">q", num) + + if num < 0: + num = (1 << (num_bytes * 8)) + num + result = bytearray(num_bytes) + for i in range(num_bytes): + result[i] = (num >> ((num_bytes - i - 1) * 8)) & 0xFF + return bytes(result) + + +def big_endian_bytes_to_unsigned_int(data: bytes | bytearray) -> int: + result = 0 + for i, b in enumerate(data): + result += b << ((len(data) - i - 1) * 8) + return result + + +def big_endian_bytes_to_signed_int(data: bytes | bytearray) -> int: + n = len(data) + if n == 1: + return int(struct.unpack(">b", data)[0]) + elif n == 2: + return int(struct.unpack(">h", data)[0]) + elif n == 4: + return int(struct.unpack(">i", data)[0]) + elif n == 8: + return int(struct.unpack(">q", data)[0]) + + # fallback for arbitrary byte lengths + result = 0 + if data[0] >= 128: + result = -1 << ((n - 1) * 8) + for i, b in enumerate(data): + result += b << ((n - i - 1) * 8) + return result + + +def little_endian_unsigned_int_to_binary_string(num: int, num_bytes: int) -> str: + b = little_endian_unsigned_int_to_binary(num, num_bytes) + return "".join(f"{byte:08b}" for byte in b) + + +def little_endian_unsigned_int_to_binary(num: int, num_bytes: int) -> bytes: + if num < 0: + raise ValueError("cannot convert negative number to binary") + if num_bytes < 1: + raise ValueError("cannot convert to binary with less than 1 byte") + if num_bytes < 8 and num >= (1 << (num_bytes * 8)): + raise ValueError(f"number is too large to fit in {num_bytes} bytes") + + if num_bytes == 1: + return struct.pack("> (i * 8)) & 0xFF + return bytes(result) + + +def little_endian_signed_int_to_binary_string(num: int, num_bytes: int) -> str: + b = little_endian_signed_int_to_binary(num, num_bytes) + return "".join(f"{byte:08b}" for byte in b) + + +def little_endian_signed_int_to_binary(num: int, num_bytes: int) -> bytes: + if num_bytes < 1: + raise ValueError("cannot convert to binary with less than 1 byte") + min_value = -(1 << ((num_bytes * 8) - 1)) + max_value = (1 << ((num_bytes * 8) - 1)) - 1 + if num < min_value or num > max_value: + raise ValueError(f"number is too large to fit in {num_bytes} bytes") + + if num_bytes == 1: + return struct.pack("> (i * 8)) & 0xFF + return bytes(result) + + +def little_endian_bytes_to_unsigned_int(data: bytes | bytearray) -> int: + result = 0 + for i, b in enumerate(data): + result += b << (i * 8) + return result + + +def little_endian_bytes_to_signed_int(data: bytes | bytearray) -> int: + n = len(data) + if n == 1: + return int(struct.unpack("= 128: + result = -1 << ((n - 1) * 8) + for i, b in enumerate(data): + result += b << (i * 8) + return result diff --git a/mapache-py/src/mapache/message.py b/mapache-py/src/mapache/message.py new file mode 100644 index 00000000..96331963 --- /dev/null +++ b/mapache-py/src/mapache/message.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from enum import IntEnum + +from mapache.binary import ( + big_endian_bytes_to_signed_int, + big_endian_bytes_to_unsigned_int, + big_endian_signed_int_to_binary, + big_endian_unsigned_int_to_binary, + little_endian_bytes_to_signed_int, + little_endian_bytes_to_unsigned_int, + little_endian_signed_int_to_binary, + little_endian_unsigned_int_to_binary, +) +from mapache.signal import Signal + + +class SignMode(IntEnum): + UNSIGNED = 0 + SIGNED = 1 + + +class Endian(IntEnum): + LITTLE_ENDIAN = 0 + BIG_ENDIAN = 1 + + +ExportSignalFunc = Callable[["Field"], list[Signal]] + + +def _default_signal_export(f: Field) -> list[Signal]: + return [Signal(name=f.name, value=float(f.value), raw_value=f.value)] + + +@dataclass +class Field: + name: str = "" + data: bytes = b"" + size: int = 0 + sign: SignMode = SignMode.UNSIGNED + endian: Endian = Endian.BIG_ENDIAN + value: int = 0 + export_signal_func: ExportSignalFunc | None = None + + def decode(self) -> Field: + if self.sign == SignMode.SIGNED and self.endian == Endian.BIG_ENDIAN: + self.value = big_endian_bytes_to_signed_int(self.data) + elif self.sign == SignMode.SIGNED and self.endian == Endian.LITTLE_ENDIAN: + self.value = little_endian_bytes_to_signed_int(self.data) + elif self.sign == SignMode.UNSIGNED and self.endian == Endian.BIG_ENDIAN: + self.value = big_endian_bytes_to_unsigned_int(self.data) + elif self.sign == SignMode.UNSIGNED and self.endian == Endian.LITTLE_ENDIAN: + self.value = little_endian_bytes_to_unsigned_int(self.data) + return self + + def encode(self) -> Field: + if self.sign == SignMode.SIGNED and self.endian == Endian.BIG_ENDIAN: + self.data = big_endian_signed_int_to_binary(self.value, self.size) + elif self.sign == SignMode.SIGNED and self.endian == Endian.LITTLE_ENDIAN: + self.data = little_endian_signed_int_to_binary(self.value, self.size) + elif self.sign == SignMode.UNSIGNED and self.endian == Endian.BIG_ENDIAN: + self.data = big_endian_unsigned_int_to_binary(self.value, self.size) + elif self.sign == SignMode.UNSIGNED and self.endian == Endian.LITTLE_ENDIAN: + self.data = little_endian_unsigned_int_to_binary(self.value, self.size) + else: + raise ValueError("invalid sign or endian") + return self + + def check_bit(self, bit: int) -> int: + byte_index = bit // 8 + bit_position = 7 - (bit % 8) + if byte_index >= len(self.data): + return 0 + return (self.data[byte_index] >> bit_position) & 1 + + def export_signals(self) -> list[Signal]: + if self.export_signal_func is None: + return _default_signal_export(self) + return self.export_signal_func(self) + + +def new_field( + name: str, + size: int, + sign: SignMode, + endian: Endian, + export_signal_func: ExportSignalFunc | None = None, +) -> Field: + return Field(name=name, size=size, sign=sign, endian=endian, export_signal_func=export_signal_func) + + +@dataclass +class Message: + fields: list[Field] = field(default_factory=list) + + def length(self) -> int: + return len(self.fields) + + def size(self) -> int: + return sum(f.size for f in self.fields) + + def fill_from_bytes(self, data: bytes | bytearray) -> None: + if len(data) != self.size(): + raise ValueError(f"invalid data length, expected {self.size()} bytes, got {len(data)}") + counter = 0 + for i, f in enumerate(self.fields): + f.data = bytes(data[counter : counter + f.size]) + counter += f.size + self.fields[i] = f.decode() + + def fill_from_ints(self, ints: list[int]) -> None: + if len(ints) != self.length(): + raise ValueError(f"invalid ints length, expected {self.length()}, got {len(ints)}") + for i, f in enumerate(self.fields): + f.value = ints[i] + self.fields[i] = f.encode() + + def export_signals(self) -> list[Signal]: + signals: list[Signal] = [] + for f in self.fields: + signals.extend(f.export_signals()) + return signals diff --git a/mapache-py/src/mapache/ping.py b/mapache-py/src/mapache/ping.py new file mode 100644 index 00000000..c4d6d74a --- /dev/null +++ b/mapache-py/src/mapache/ping.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Ping: + vehicle_id: str = "" + ping: int = 0 + pong: int = 0 + latency: int = 0 diff --git a/mapache-py/src/mapache/py.typed b/mapache-py/src/mapache/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/mapache-py/src/mapache/signal.py b/mapache-py/src/mapache/signal.py new file mode 100644 index 00000000..69d3f9d8 --- /dev/null +++ b/mapache-py/src/mapache/signal.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime + + +@dataclass +class Signal: + id: str = "" + timestamp: int = 0 + vehicle_id: str = "" + name: str = "" + value: float = 0.0 + raw_value: int = 0 + produced_at: datetime = field(default_factory=lambda: datetime.min) + created_at: datetime = field(default_factory=lambda: datetime.min) diff --git a/mapache-py/src/mapache/vehicle.py b/mapache-py/src/mapache/vehicle.py new file mode 100644 index 00000000..530fa892 --- /dev/null +++ b/mapache-py/src/mapache/vehicle.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime + + +@dataclass +class Vehicle: + id: str = "" + name: str = "" + description: str = "" + type: str = "" + upload_key: int = 0 + updated_at: datetime = field(default_factory=lambda: datetime.min) + created_at: datetime = field(default_factory=lambda: datetime.min) + + +@dataclass +class Session: + id: str = "" + vehicle_id: str = "" + name: str = "" + description: str = "" + start_time: datetime = field(default_factory=lambda: datetime.min) + end_time: datetime = field(default_factory=lambda: datetime.min) + markers: list[Marker] = field(default_factory=list) + segments: list[Segment] = field(default_factory=list) + + +@dataclass +class Marker: + id: str = "" + session_id: str = "" + name: str = "" + timestamp: datetime = field(default_factory=lambda: datetime.min) + + +@dataclass +class Segment: + number: int = 0 + start_time: datetime = field(default_factory=lambda: datetime.min) + end_time: datetime = field(default_factory=lambda: datetime.min) + + +def derive_segments(session: Session) -> list[Segment]: + if not session.markers: + return [Segment(number=1, start_time=session.start_time, end_time=session.end_time)] + + sorted_markers = sorted(session.markers, key=lambda m: m.timestamp) + + segments: list[Segment] = [] + segments.append(Segment(number=1, start_time=session.start_time, end_time=sorted_markers[0].timestamp)) + for i in range(len(sorted_markers) - 1): + segments.append( + Segment( + number=i + 2, + start_time=sorted_markers[i].timestamp, + end_time=sorted_markers[i + 1].timestamp, + ) + ) + segments.append( + Segment( + number=len(sorted_markers) + 1, + start_time=sorted_markers[-1].timestamp, + end_time=session.end_time, + ) + ) + + return segments diff --git a/mapache-py/tests/__init__.py b/mapache-py/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mapache-py/tests/test_binary.py b/mapache-py/tests/test_binary.py new file mode 100644 index 00000000..bd172893 --- /dev/null +++ b/mapache-py/tests/test_binary.py @@ -0,0 +1,362 @@ +import struct + +import pytest + +from mapache import ( + big_endian_bytes_to_signed_int, + big_endian_bytes_to_unsigned_int, + big_endian_signed_int_to_binary, + big_endian_signed_int_to_binary_string, + big_endian_unsigned_int_to_binary, + big_endian_unsigned_int_to_binary_string, + little_endian_bytes_to_signed_int, + little_endian_bytes_to_unsigned_int, + little_endian_signed_int_to_binary, + little_endian_signed_int_to_binary_string, + little_endian_unsigned_int_to_binary, + little_endian_unsigned_int_to_binary_string, +) + + +class TestBigEndianUnsignedIntToBinaryString: + def test_38134_1_byte(self) -> None: + with pytest.raises(ValueError): + big_endian_unsigned_int_to_binary_string(38134, 1) + + def test_38134_2_byte(self) -> None: + assert big_endian_unsigned_int_to_binary_string(38134, 2) == "1001010011110110" + + +class TestBigEndianUnsignedIntToBinary: + def test_negative_number(self) -> None: + with pytest.raises(ValueError): + big_endian_unsigned_int_to_binary(-1, 1) + + def test_0_bytes(self) -> None: + with pytest.raises(ValueError): + big_endian_unsigned_int_to_binary(100, 0) + + def test_number_too_large(self) -> None: + with pytest.raises(ValueError): + big_endian_unsigned_int_to_binary(31241, 1) + + def test_number_too_large_2(self) -> None: + with pytest.raises(ValueError): + big_endian_unsigned_int_to_binary(3172123, 2) + + def test_0_1_byte(self) -> None: + assert big_endian_unsigned_int_to_binary(0, 1) == bytes([0]) + + def test_123_1_byte(self) -> None: + assert big_endian_unsigned_int_to_binary(123, 1) == bytes([123]) + + def test_255_1_byte(self) -> None: + assert big_endian_unsigned_int_to_binary(255, 1) == bytes([255]) + + def test_172_2_byte(self) -> None: + assert big_endian_unsigned_int_to_binary(172, 2) == struct.pack(">H", 172) + + def test_38134_2_byte(self) -> None: + assert big_endian_unsigned_int_to_binary(38134, 2) == struct.pack(">H", 38134) + + def test_429496295_4_byte(self) -> None: + assert big_endian_unsigned_int_to_binary(429496295, 4) == struct.pack(">I", 429496295) + + def test_44009551615_6_byte(self) -> None: + assert big_endian_unsigned_int_to_binary(44009551615, 6) == bytes([0, 10, 63, 44, 118, 255]) + + def test_18446744009551615_8_byte(self) -> None: + assert big_endian_unsigned_int_to_binary(18446744009551615, 8) == struct.pack(">Q", 18446744009551615) + + +class TestBigEndianSignedIntToBinaryString: + def test_32767_1_byte(self) -> None: + with pytest.raises(ValueError): + big_endian_signed_int_to_binary_string(32767, 1) + + def test_32767_2_byte(self) -> None: + assert big_endian_signed_int_to_binary_string(32767, 2) == "0111111111111111" + + +class TestBigEndianSignedIntToBinary: + def test_0_bytes(self) -> None: + with pytest.raises(ValueError): + big_endian_signed_int_to_binary(100, 0) + + def test_number_too_large(self) -> None: + with pytest.raises(ValueError): + big_endian_signed_int_to_binary(31241, 1) + + def test_number_too_large_2(self) -> None: + with pytest.raises(ValueError): + big_endian_signed_int_to_binary(3172123, 2) + + def test_0_1_byte(self) -> None: + assert big_endian_signed_int_to_binary(0, 1) == bytes([0]) + + def test_123_1_byte(self) -> None: + assert big_endian_signed_int_to_binary(123, 1) == bytes([123]) + + def test_255_1_byte(self) -> None: + with pytest.raises(ValueError): + big_endian_signed_int_to_binary(255, 1) + + def test_172_2_byte(self) -> None: + assert big_endian_signed_int_to_binary(172, 2) == struct.pack(">h", 172) + + def test_32767_2_byte(self) -> None: + assert big_endian_signed_int_to_binary(32767, 2) == struct.pack(">h", 32767) + + def test_neg_32767_2_byte(self) -> None: + assert big_endian_signed_int_to_binary(-32767, 2) == struct.pack(">h", -32767) + + def test_429496295_4_byte(self) -> None: + assert big_endian_signed_int_to_binary(429496295, 4) == struct.pack(">i", 429496295) + + def test_neg_429496295_4_byte(self) -> None: + assert big_endian_signed_int_to_binary(-429496295, 4) == struct.pack(">i", -429496295) + + def test_44009551615_6_byte(self) -> None: + assert big_endian_signed_int_to_binary(44009551615, 6) == bytes([0, 10, 63, 44, 118, 255]) + + def test_18446744009551615_8_byte(self) -> None: + assert big_endian_signed_int_to_binary(18446744009551615, 8) == struct.pack(">q", 18446744009551615) + + def test_neg_18446744009551615_8_byte(self) -> None: + assert big_endian_signed_int_to_binary(-18446744009551615, 8) == struct.pack(">q", -18446744009551615) + + +class TestBigEndianBytesToUnsignedInt: + def test_0_1_byte(self) -> None: + assert big_endian_bytes_to_unsigned_int(bytes([0])) == 0 + + def test_123_1_byte(self) -> None: + assert big_endian_bytes_to_unsigned_int(bytes([123])) == 123 + + def test_255_1_byte(self) -> None: + assert big_endian_bytes_to_unsigned_int(bytes([255])) == 255 + + def test_172_2_byte(self) -> None: + assert big_endian_bytes_to_unsigned_int(bytes([0, 172])) == 172 + + def test_38134_2_byte(self) -> None: + assert big_endian_bytes_to_unsigned_int(bytes([148, 246])) == 38134 + + def test_429496295_4_byte(self) -> None: + assert big_endian_bytes_to_unsigned_int(bytes([25, 153, 151, 231])) == 429496295 + + def test_44009551615_6_byte(self) -> None: + assert big_endian_bytes_to_unsigned_int(bytes([0, 10, 63, 44, 118, 255])) == 44009551615 + + def test_18446744009551615_8_byte(self) -> None: + assert big_endian_bytes_to_unsigned_int(bytes([0, 65, 137, 55, 71, 243, 174, 255])) == 18446744009551615 + + +class TestBigEndianBytesToSignedInt: + def test_0_1_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([0])) == 0 + + def test_123_1_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([123])) == 123 + + def test_255_1_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([255])) == -1 + + def test_172_2_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([0, 172])) == 172 + + def test_32767_2_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([127, 255])) == 32767 + + def test_neg_32767_2_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([128, 1])) == -32767 + + def test_429496295_4_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([25, 153, 151, 231])) == 429496295 + + def test_neg_429496295_4_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([230, 102, 104, 25])) == -429496295 + + def test_44009551615_6_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([0, 10, 63, 44, 118, 255])) == 44009551615 + + def test_279319963006464_6_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([255, 10, 63, 44, 118, 0])) == 279319963006464 + + def test_18446744009551615_8_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([0, 65, 137, 55, 71, 243, 174, 255])) == 18446744009551615 + + def test_neg_18446744009551615_8_byte(self) -> None: + assert big_endian_bytes_to_signed_int(bytes([255, 190, 118, 200, 184, 12, 81, 1])) == -18446744009551615 + + +class TestLittleEndianUnsignedIntToBinaryString: + def test_38134_1_byte(self) -> None: + with pytest.raises(ValueError): + little_endian_unsigned_int_to_binary_string(38134, 1) + + def test_38134_2_byte(self) -> None: + assert little_endian_unsigned_int_to_binary_string(38134, 2) == "1111011010010100" + + +class TestLittleEndianUnsignedIntToBinary: + def test_negative_number(self) -> None: + with pytest.raises(ValueError): + little_endian_unsigned_int_to_binary(-1, 1) + + def test_0_bytes(self) -> None: + with pytest.raises(ValueError): + little_endian_unsigned_int_to_binary(100, 0) + + def test_number_too_large(self) -> None: + with pytest.raises(ValueError): + little_endian_unsigned_int_to_binary(31241, 1) + + def test_number_too_large_2(self) -> None: + with pytest.raises(ValueError): + little_endian_unsigned_int_to_binary(3172123, 2) + + def test_0_1_byte(self) -> None: + assert little_endian_unsigned_int_to_binary(0, 1) == bytes([0]) + + def test_123_1_byte(self) -> None: + assert little_endian_unsigned_int_to_binary(123, 1) == bytes([123]) + + def test_255_1_byte(self) -> None: + assert little_endian_unsigned_int_to_binary(255, 1) == bytes([255]) + + def test_172_2_byte(self) -> None: + assert little_endian_unsigned_int_to_binary(172, 2) == struct.pack(" None: + assert little_endian_unsigned_int_to_binary(38134, 2) == struct.pack(" None: + assert little_endian_unsigned_int_to_binary(429496295, 4) == struct.pack(" None: + assert little_endian_unsigned_int_to_binary(44009551615, 6) == bytes([255, 118, 44, 63, 10, 0]) + + def test_18446744009551615_8_byte(self) -> None: + assert little_endian_unsigned_int_to_binary(18446744009551615, 8) == struct.pack(" None: + with pytest.raises(ValueError): + little_endian_signed_int_to_binary_string(32767, 1) + + def test_32767_2_byte(self) -> None: + assert little_endian_signed_int_to_binary_string(32767, 2) == "1111111101111111" + + +class TestLittleEndianSignedIntToBinary: + def test_0_bytes(self) -> None: + with pytest.raises(ValueError): + little_endian_signed_int_to_binary(100, 0) + + def test_number_too_large(self) -> None: + with pytest.raises(ValueError): + little_endian_signed_int_to_binary(31241, 1) + + def test_number_too_large_2(self) -> None: + with pytest.raises(ValueError): + little_endian_signed_int_to_binary(3172123, 2) + + def test_0_1_byte(self) -> None: + assert little_endian_signed_int_to_binary(0, 1) == bytes([0]) + + def test_123_1_byte(self) -> None: + assert little_endian_signed_int_to_binary(123, 1) == bytes([123]) + + def test_255_1_byte(self) -> None: + with pytest.raises(ValueError): + little_endian_signed_int_to_binary(255, 1) + + def test_172_2_byte(self) -> None: + assert little_endian_signed_int_to_binary(172, 2) == struct.pack(" None: + assert little_endian_signed_int_to_binary(32767, 2) == struct.pack(" None: + assert little_endian_signed_int_to_binary(-32767, 2) == struct.pack(" None: + assert little_endian_signed_int_to_binary(429496295, 4) == struct.pack(" None: + assert little_endian_signed_int_to_binary(-429496295, 4) == struct.pack(" None: + assert little_endian_signed_int_to_binary(44009551615, 6) == bytes([255, 118, 44, 63, 10, 0]) + + def test_18446744009551615_8_byte(self) -> None: + assert little_endian_signed_int_to_binary(18446744009551615, 8) == struct.pack(" None: + assert little_endian_signed_int_to_binary(-18446744009551615, 8) == struct.pack(" None: + assert little_endian_bytes_to_unsigned_int(bytes([0])) == 0 + + def test_123_1_byte(self) -> None: + assert little_endian_bytes_to_unsigned_int(bytes([123])) == 123 + + def test_255_1_byte(self) -> None: + assert little_endian_bytes_to_unsigned_int(bytes([255])) == 255 + + def test_172_2_byte(self) -> None: + assert little_endian_bytes_to_unsigned_int(bytes([172, 0])) == 172 + + def test_38134_2_byte(self) -> None: + assert little_endian_bytes_to_unsigned_int(bytes([246, 148])) == 38134 + + def test_429496295_4_byte(self) -> None: + assert little_endian_bytes_to_unsigned_int(bytes([231, 151, 153, 25])) == 429496295 + + def test_44009551615_6_byte(self) -> None: + assert little_endian_bytes_to_unsigned_int(bytes([255, 118, 44, 63, 10, 0])) == 44009551615 + + def test_18446744009551615_8_byte(self) -> None: + assert little_endian_bytes_to_unsigned_int(bytes([255, 174, 243, 71, 55, 137, 65, 0])) == 18446744009551615 + + +class TestLittleEndianBytesToSignedInt: + def test_0_1_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([0])) == 0 + + def test_123_1_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([123])) == 123 + + def test_255_1_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([255])) == -1 + + def test_172_2_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([172, 0])) == 172 + + def test_32767_2_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([255, 127])) == 32767 + + def test_neg_32767_2_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([1, 128])) == -32767 + + def test_429496295_4_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([231, 151, 153, 25])) == 429496295 + + def test_neg_429496295_4_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([25, 104, 102, 230])) == -429496295 + + def test_44009551615_6_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([255, 118, 44, 63, 10, 0])) == 44009551615 + + def test_279319963006464_6_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([0, 118, 44, 63, 10, 255])) == 279319963006464 + + def test_18446744009551615_8_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([255, 174, 243, 71, 55, 137, 65, 0])) == 18446744009551615 + + def test_neg_18446744009551615_8_byte(self) -> None: + assert little_endian_bytes_to_signed_int(bytes([1, 81, 12, 184, 200, 118, 190, 255])) == -18446744009551615 diff --git a/mapache-py/tests/test_message.py b/mapache-py/tests/test_message.py new file mode 100644 index 00000000..f810c8a7 --- /dev/null +++ b/mapache-py/tests/test_message.py @@ -0,0 +1,279 @@ +import pytest + +from mapache import Endian, Field, Message, Signal, SignMode, new_field + + +def _ecu_status_message() -> Message: + return Message( + fields=[ + new_field("ecu_state", 1, SignMode.UNSIGNED, Endian.BIG_ENDIAN), + new_field( + "ecu_status_flags", + 3, + SignMode.UNSIGNED, + Endian.BIG_ENDIAN, + lambda f: [ + Signal( + name=name, + value=float(f.check_bit(i)), + raw_value=f.check_bit(i), + ) + for i, name in enumerate( + [ + "ecu_status_acu", + "ecu_status_inv_one", + "ecu_status_inv_two", + "ecu_status_inv_three", + "ecu_status_inv_four", + "ecu_status_fan_one", + "ecu_status_fan_two", + "ecu_status_fan_three", + "ecu_status_fan_four", + "ecu_status_fan_five", + "ecu_status_fan_six", + "ecu_status_fan_seven", + "ecu_status_fan_eight", + "ecu_status_dash", + "ecu_status_steering", + ] + ) + ], + ), + new_field( + "ecu_maps", + 1, + SignMode.UNSIGNED, + Endian.BIG_ENDIAN, + lambda f: [ + Signal(name="ecu_power_level", value=float((f.value >> 4) & 0x0F), raw_value=(f.value >> 4) & 0x0F), + Signal(name="ecu_torque_map", value=float(f.value & 0x0F), raw_value=f.value & 0x0F), + ], + ), + new_field( + "ecu_max_cell_temp", + 1, + SignMode.UNSIGNED, + Endian.BIG_ENDIAN, + lambda f: [ + Signal(name="ecu_max_cell_temp", value=float(f.value) * 0.25, raw_value=f.value), + ], + ), + new_field( + "ecu_acu_state_of_charge", + 1, + SignMode.UNSIGNED, + Endian.BIG_ENDIAN, + lambda f: [ + Signal(name="ecu_acu_state_of_charge", value=float(f.value) * 20 / 51, raw_value=f.value), + ], + ), + new_field( + "ecu_glv_state_of_charge", + 1, + SignMode.UNSIGNED, + Endian.BIG_ENDIAN, + lambda f: [ + Signal(name="ecu_glv_state_of_charge", value=float(f.value) * 20 / 51, raw_value=f.value), + ], + ), + ] + ) + + +class TestMessage: + def test_invalid_byte_length(self) -> None: + msg = _ecu_status_message() + with pytest.raises(ValueError): + msg.fill_from_bytes(bytes([0, 0])) + + def test_zero_values(self) -> None: + msg = _ecu_status_message() + msg.fill_from_bytes(bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + signals = msg.export_signals() + for signal in signals: + assert signal.value == 0 + assert signal.raw_value == 0 + + def test_nonzero_values(self) -> None: + msg = _ecu_status_message() + msg.fill_from_bytes(bytes([0x12, 0x42, 0xFF, 0x00, 0x31, 0x82, 0x58, 0x72])) + signals = msg.export_signals() + expected_values = [ + 18, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 32.5, + 34.509804, + 44.705882, + ] + for i, signal in enumerate(signals): + assert int(signal.value) == int(expected_values[i]) + + +class TestNewField: + def test_basic(self) -> None: + field = new_field("test", 1, SignMode.UNSIGNED, Endian.BIG_ENDIAN) + assert field.size == 1 + assert field.sign == SignMode.UNSIGNED + + +class TestDecode: + @pytest.mark.parametrize( + "name,field_kwargs,expected", + [ + ( + "Signed BigEndian Positive", + dict(data=bytes([0x12, 0x34]), size=2, sign=SignMode.SIGNED, endian=Endian.BIG_ENDIAN), + 0x1234, + ), + ( + "Signed BigEndian Negative", + dict(data=bytes([0xFF, 0xFE]), size=2, sign=SignMode.SIGNED, endian=Endian.BIG_ENDIAN), + -2, + ), + ( + "Signed LittleEndian Positive", + dict(data=bytes([0x34, 0x12]), size=2, sign=SignMode.SIGNED, endian=Endian.LITTLE_ENDIAN), + 0x1234, + ), + ( + "Signed LittleEndian Negative", + dict(data=bytes([0xFE, 0xFF]), size=2, sign=SignMode.SIGNED, endian=Endian.LITTLE_ENDIAN), + -2, + ), + ( + "Unsigned BigEndian", + dict(data=bytes([0xFF, 0xFE]), size=2, sign=SignMode.UNSIGNED, endian=Endian.BIG_ENDIAN), + 0xFFFE, + ), + ( + "Unsigned LittleEndian", + dict(data=bytes([0xFE, 0xFF]), size=2, sign=SignMode.UNSIGNED, endian=Endian.LITTLE_ENDIAN), + 0xFFFE, + ), + ( + "Single Byte Signed Positive", + dict(data=bytes([0x7F]), size=1, sign=SignMode.SIGNED, endian=Endian.BIG_ENDIAN), + 127, + ), + ( + "Single Byte Signed Negative", + dict(data=bytes([0xCF]), size=1, sign=SignMode.SIGNED, endian=Endian.BIG_ENDIAN), + -49, + ), + ( + "Four Bytes Unsigned BigEndian", + dict(data=bytes([0x12, 0x34, 0x56, 0x78]), size=4, sign=SignMode.UNSIGNED, endian=Endian.BIG_ENDIAN), + 0x12345678, + ), + ( + "Four Bytes Unsigned LittleEndian", + dict(data=bytes([0x78, 0x56, 0x34, 0x12]), size=4, sign=SignMode.UNSIGNED, endian=Endian.LITTLE_ENDIAN), + 0x12345678, + ), + ], + ) + def test_decode(self, name: str, field_kwargs: dict, expected: int) -> None: # type: ignore[type-arg] + f = Field(**field_kwargs) + result = f.decode() + assert result.value == expected, f"{name}: expected {expected}, got {result.value}" + + +class TestEncode: + @pytest.mark.parametrize( + "name,field_kwargs,expected", + [ + ( + "Signed BigEndian Positive", + dict(value=0x1234, size=2, sign=SignMode.SIGNED, endian=Endian.BIG_ENDIAN), + bytes([0x12, 0x34]), + ), + ( + "Signed BigEndian Negative", + dict(value=-2, size=2, sign=SignMode.SIGNED, endian=Endian.BIG_ENDIAN), + bytes([0xFF, 0xFE]), + ), + ( + "Signed LittleEndian Positive", + dict(value=0x1234, size=2, sign=SignMode.SIGNED, endian=Endian.LITTLE_ENDIAN), + bytes([0x34, 0x12]), + ), + ( + "Signed LittleEndian Negative", + dict(value=-2, size=2, sign=SignMode.SIGNED, endian=Endian.LITTLE_ENDIAN), + bytes([0xFE, 0xFF]), + ), + ( + "Unsigned BigEndian", + dict(value=0xFFFE, size=2, sign=SignMode.UNSIGNED, endian=Endian.BIG_ENDIAN), + bytes([0xFF, 0xFE]), + ), + ( + "Unsigned LittleEndian", + dict(value=0xFFFE, size=2, sign=SignMode.UNSIGNED, endian=Endian.LITTLE_ENDIAN), + bytes([0xFE, 0xFF]), + ), + ( + "Single Byte Signed Positive", + dict(value=127, size=1, sign=SignMode.SIGNED, endian=Endian.BIG_ENDIAN), + bytes([0x7F]), + ), + ( + "Single Byte Signed Negative", + dict(value=-49, size=1, sign=SignMode.SIGNED, endian=Endian.BIG_ENDIAN), + bytes([0xCF]), + ), + ( + "Four Bytes Unsigned BigEndian", + dict(value=0x12345678, size=4, sign=SignMode.UNSIGNED, endian=Endian.BIG_ENDIAN), + bytes([0x12, 0x34, 0x56, 0x78]), + ), + ( + "Four Bytes Unsigned LittleEndian", + dict(value=0x12345678, size=4, sign=SignMode.UNSIGNED, endian=Endian.LITTLE_ENDIAN), + bytes([0x78, 0x56, 0x34, 0x12]), + ), + ], + ) + def test_encode(self, name: str, field_kwargs: dict, expected: bytes) -> None: # type: ignore[type-arg] + f = Field(**field_kwargs) + result = f.encode() + assert result.data == expected, f"{name}: expected {expected!r}, got {result.data!r}" + + def test_value_too_large(self) -> None: + f = Field(value=0x1234, size=1, sign=SignMode.UNSIGNED, endian=Endian.BIG_ENDIAN) + with pytest.raises(ValueError): + f.encode() + + def test_negative_unsigned(self) -> None: + f = Field(value=-1, size=2, sign=SignMode.UNSIGNED, endian=Endian.BIG_ENDIAN) + with pytest.raises(ValueError): + f.encode() + + def test_invalid_sign(self) -> None: + with pytest.raises(ValueError): + SignMode(3) + + +class TestCheckBit: + def test_check_bit(self) -> None: + test_bytes = bytes([0x12, 0x34]) + f = Field(data=test_bytes, size=len(test_bytes)) + for i in range(f.size * 8): + expected = (test_bytes[i // 8] >> (7 - i % 8)) & 1 + assert f.check_bit(i) == expected diff --git a/mapache-py/tests/test_vehicle.py b/mapache-py/tests/test_vehicle.py new file mode 100644 index 00000000..fe4b4455 --- /dev/null +++ b/mapache-py/tests/test_vehicle.py @@ -0,0 +1,57 @@ +from datetime import datetime, timezone + +from mapache import Marker, Session, derive_segments + + +class TestDeriveSegmentsNoMarkers: + def test_single_segment(self) -> None: + start = datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + end = datetime(2026, 1, 1, 1, 0, 0, tzinfo=timezone.utc) + session = Session(start_time=start, end_time=end) + + segments = derive_segments(session) + assert len(segments) == 1 + assert segments[0].number == 1 + assert segments[0].start_time == start + assert segments[0].end_time == end + + +class TestDeriveSegmentsOneMarker: + def test_two_segments(self) -> None: + start = datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + mid = datetime(2026, 1, 1, 0, 30, 0, tzinfo=timezone.utc) + end = datetime(2026, 1, 1, 1, 0, 0, tzinfo=timezone.utc) + session = Session(start_time=start, end_time=end, markers=[Marker(timestamp=mid)]) + + segments = derive_segments(session) + assert len(segments) == 2 + assert segments[0].number == 1 + assert segments[1].number == 2 + assert segments[0].start_time == start + assert segments[0].end_time == mid + assert segments[1].start_time == mid + assert segments[1].end_time == end + + +class TestDeriveSegmentsMultipleMarkers: + def test_four_segments_with_sorting(self) -> None: + start = datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + m1 = datetime(2026, 1, 1, 0, 10, 0, tzinfo=timezone.utc) + m2 = datetime(2026, 1, 1, 0, 20, 0, tzinfo=timezone.utc) + m3 = datetime(2026, 1, 1, 0, 30, 0, tzinfo=timezone.utc) + end = datetime(2026, 1, 1, 1, 0, 0, tzinfo=timezone.utc) + + session = Session( + start_time=start, + end_time=end, + markers=[Marker(timestamp=m3), Marker(timestamp=m1), Marker(timestamp=m2)], + ) + + segments = derive_segments(session) + assert len(segments) == 4 + for i, seg in enumerate(segments): + assert seg.number == i + 1 + assert segments[0].start_time == start and segments[0].end_time == m1 + assert segments[1].start_time == m1 and segments[1].end_time == m2 + assert segments[2].start_time == m2 and segments[2].end_time == m3 + assert segments[3].start_time == m3 and segments[3].end_time == end From a3ccf6085c6135527a56c67782b8be59c0836087 Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Thu, 12 Mar 2026 10:47:17 -0700 Subject: [PATCH 2/4] chore: move mapache-py CI to global workflows directory --- .../workflows/mapache-py.yml | 2 +- mapache-py/.github/workflows/publish.yml | 24 ------------------- 2 files changed, 1 insertion(+), 25 deletions(-) rename mapache-py/.github/workflows/ci.yml => .github/workflows/mapache-py.yml (98%) delete mode 100644 mapache-py/.github/workflows/publish.yml diff --git a/mapache-py/.github/workflows/ci.yml b/.github/workflows/mapache-py.yml similarity index 98% rename from mapache-py/.github/workflows/ci.yml rename to .github/workflows/mapache-py.yml index 02db8545..7ec34511 100644 --- a/mapache-py/.github/workflows/ci.yml +++ b/.github/workflows/mapache-py.yml @@ -1,4 +1,4 @@ -name: CI +name: mapache-py on: push: diff --git a/mapache-py/.github/workflows/publish.yml b/mapache-py/.github/workflows/publish.yml deleted file mode 100644 index 0b471a11..00000000 --- a/mapache-py/.github/workflows/publish.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Publish to PyPI - -on: - release: - types: [published] - -defaults: - run: - working-directory: mapache-py - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - run: pip install build twine - - run: python -m build - - run: twine upload dist/* - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} From 29e2f865f93bac9ba3f5d8963b6beaae26ef2eef Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Thu, 12 Mar 2026 11:02:37 -0700 Subject: [PATCH 3/4] chore: consolidate mapache-py CI into lint and test jobs --- .github/workflows/mapache-py.yml | 45 ++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/.github/workflows/mapache-py.yml b/.github/workflows/mapache-py.yml index 7ec34511..97417304 100644 --- a/.github/workflows/mapache-py.yml +++ b/.github/workflows/mapache-py.yml @@ -19,33 +19,40 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - run: pip install ruff - - run: ruff format --check src/ tests/ - - run: ruff check src/ tests/ - typecheck: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.13" - - run: pip install -e ".[dev]" - - run: mypy src/ + + - name: Install dependencies + run: pip install -e ".[dev]" + + - name: Lint + run: | + ruff format --check src/ tests/ + ruff check src/ tests/ + + - name: Type check + run: mypy src/ test: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - run: pip install -e ".[dev]" - - run: pytest tests/ -v + python-version: | + 3.10 + 3.11 + 3.12 + 3.13 + + - name: Test + run: | + for ver in 3.10 3.11 3.12 3.13; do + echo "::group::Python $ver" + python$ver -m pip install -e ".[dev]" + python$ver -m pytest tests/ -v + echo "::endgroup::" + done From 3175e396ccbf38a914503b2a354e5d0e0fd7b28f Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Thu, 12 Mar 2026 11:16:00 -0700 Subject: [PATCH 4/4] ci: add mapache-go workflow for vet and test --- .github/workflows/mapache-go.yml | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/mapache-go.yml diff --git a/.github/workflows/mapache-go.yml b/.github/workflows/mapache-go.yml new file mode 100644 index 00000000..565def24 --- /dev/null +++ b/.github/workflows/mapache-go.yml @@ -0,0 +1,40 @@ +name: mapache-go + +on: + push: + branches: [main] + paths: + - "mapache-go/**" + pull_request: + branches: [main] + paths: + - "mapache-go/**" + +defaults: + run: + working-directory: mapache-go + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.26" + + - name: Vet + run: go vet ./... + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.26" + + - name: Test + run: go test ./... -v