From 9638bb36fed6cafbd69683246cd2465aa359cc79 Mon Sep 17 00:00:00 2001 From: Cameron Devine Date: Thu, 30 May 2024 10:27:08 -0700 Subject: [PATCH] a huge commit where this library was refactored into Pigeon and this repo is now just message definitions --- Dockerfile.publisher | 9 -- Dockerfile.subscriber | 8 -- LICENSE | 11 ++ README.md | 6 + TEM_comms/__init__.py | 38 ++++- TEM_comms/__main__.py | 55 -------- TEM_comms/{msgs => }/buffer.py | 2 +- TEM_comms/{msgs => }/camera.py | 2 +- TEM_comms/client.py | 139 ------------------- TEM_comms/exceptions.py | 5 - TEM_comms/logging.py | 13 -- TEM_comms/msgs/__init__.py | 29 ---- TEM_comms/msgs/base.py | 12 -- TEM_comms/{msgs => }/scope.py | 2 +- TEM_comms/{msgs => }/stage/__init__.py | 0 TEM_comms/{msgs => }/stage/aperture.py | 2 +- TEM_comms/{msgs => }/stage/motion.py | 2 +- TEM_comms/{msgs => }/stage/rotation.py | 2 +- TEM_comms/{msgs => }/tile/__init__.py | 2 +- TEM_comms/{msgs => }/tile/statistics.py | 2 +- dev-requirements.in | 4 - dev-requirements.txt | 16 --- dev.env | 3 - docker-compose.yml | 37 ----- examples/publisher.py | 18 --- examples/server.sh | 1 - examples/subscriber.py | 22 --- pyproject.toml | 21 ++- requirements.in | 3 - requirements.txt | 17 --- tests/test_base_message.py | 38 ----- tests/test_buffer.py | 2 +- tests/test_client.py | 177 ------------------------ {test => tests}/test_scope_command.py | 2 +- 34 files changed, 72 insertions(+), 630 deletions(-) delete mode 100644 Dockerfile.publisher delete mode 100644 Dockerfile.subscriber create mode 100644 LICENSE create mode 100644 README.md delete mode 100644 TEM_comms/__main__.py rename TEM_comms/{msgs => }/buffer.py (73%) rename TEM_comms/{msgs => }/camera.py (92%) delete mode 100644 TEM_comms/client.py delete mode 100644 TEM_comms/exceptions.py delete mode 100644 TEM_comms/logging.py delete mode 100644 TEM_comms/msgs/__init__.py delete mode 100644 TEM_comms/msgs/base.py rename TEM_comms/{msgs => }/scope.py (93%) rename TEM_comms/{msgs => }/stage/__init__.py (100%) rename TEM_comms/{msgs => }/stage/aperture.py (80%) rename TEM_comms/{msgs => }/stage/motion.py (85%) rename TEM_comms/{msgs => }/stage/rotation.py (87%) rename TEM_comms/{msgs => }/tile/__init__.py (92%) rename TEM_comms/{msgs => }/tile/statistics.py (86%) delete mode 100644 dev-requirements.in delete mode 100644 dev-requirements.txt delete mode 100644 dev.env delete mode 100644 docker-compose.yml delete mode 100644 examples/publisher.py delete mode 100755 examples/server.sh delete mode 100644 examples/subscriber.py delete mode 100644 requirements.in delete mode 100644 requirements.txt delete mode 100644 tests/test_base_message.py delete mode 100644 tests/test_client.py rename {test => tests}/test_scope_command.py (91%) diff --git a/Dockerfile.publisher b/Dockerfile.publisher deleted file mode 100644 index 6a29956..0000000 --- a/Dockerfile.publisher +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.11-slim-bookworm - -COPY . /app - -WORKDIR /app -RUN pip install . - - -CMD ["python", "examples/publisher.py"] \ No newline at end of file diff --git a/Dockerfile.subscriber b/Dockerfile.subscriber deleted file mode 100644 index ec44349..0000000 --- a/Dockerfile.subscriber +++ /dev/null @@ -1,8 +0,0 @@ -FROM python:3.11-slim-bookworm - -COPY . /app - -WORKDIR /app -RUN pip install . - -CMD ["python", "examples/subscriber.py"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..54aac36 --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +Copyright 2024 Allen Institute + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a25385e --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/AllenInstitute/TEM_comms/test.yaml)](https://github.com/AllenInstitute/TEM_comms/actions) +[![PyPI - Version](https://img.shields.io/pypi/v/pigeon-tem-comms)](https://pypi.org/project/pigeon-tem-comms/) + +# TEM Comms + +TEM Comms is a set of [Pydantic](https://docs.pydantic.dev/latest/) models to be used as messages with [Pigeon](https://github.com/AllenInstitute/pigeon) for the [Allen Institute's](https://alleninstitute.org/) next generation transmission electron microscope (TEM) [image acquisition system](https://alleninstitute.github.io/TEM_architecture/). \ No newline at end of file diff --git a/TEM_comms/__init__.py b/TEM_comms/__init__.py index 8533382..f8b77df 100644 --- a/TEM_comms/__init__.py +++ b/TEM_comms/__init__.py @@ -1 +1,37 @@ -from .client import TEMComms +from . import buffer +from . import camera +from . import scope +from . import stage +from . import tile + +import importlib.metadata + + +__version__ = importlib.metadata.version("TEM_comms") + +topics = { + "buffer.status": buffer.Status, + "camera.command": camera.Command, + "camera.image": camera.Image, + "camera.settings": camera.Settings, + "camera.status": camera.Status, + "scope.command": scope.Command, + "scope.status": scope.Status, + "stage.aperture.command": stage.aperture.Command, + "stage.aperture.status": stage.aperture.Status, + "stage.motion.command": stage.motion.Command, + "stage.motion.status": stage.motion.Status, + "stage.rotation.command": stage.rotation.Command, + "stage.rotation.status": stage.rotation.Status, + "tile.jpeg": tile.JPEG, + "tile.minimap": tile.Minimap, + "tile.processed": tile.Processed, + "tile.raw": tile.Raw, + "tile.statistics.focus": tile.statistics.Focus, + "tile.statistics.histogram": tile.statistics.Histogram, + "tile.statistics.min_max_mean": tile.statistics.MinMaxMean, + "tile.transform": tile.Transform, +} + + +msgs = (topics, __version__) \ No newline at end of file diff --git a/TEM_comms/__main__.py b/TEM_comms/__main__.py deleted file mode 100644 index 31bba96..0000000 --- a/TEM_comms/__main__.py +++ /dev/null @@ -1,55 +0,0 @@ -from .client import TEMComms -import argparse -import yaml - - -def callback_factory(topic): - def callback(data): - print("Recieved message on topic '{}':".format(topic)) - print(data) - return callback - - -def main(): - parser = argparse.ArgumentParser(prog="TEM_comm_cli") - parser.add_argument("--host", type=str, default="127.0.0.1", help="The message broker to connect to.") - parser.add_argument("--port", type=int, default=61616, help="The port to use for the connection.") - parser.add_argument("--username", type=str, help="The username to use when connecting to the STOMP server.") - parser.add_argument("--password", type=str, help="The password to use when connecting to the STOMP server.") - parser.add_argument("-p", "--publish", type=str, help="The topic to publish a message to.") - parser.add_argument("-d", "--data", type=str, help="The YAML/JSON formatted data to publish.") - parser.add_argument("-s", "--subscribe", type=str, action="append", default=[], help="The topic to subscribe to.") - - args = parser.parse_args() - - if args.publish is None and args.subscribe is None: - print("No action specified.") - return - - if args.publish and args.data is None: - print("Must also specify data to publish.") - return - - if args.data and args.publish is None: - print("Most also specify topic to publish data to.") - return - - connection = TEMComms(args.host, args.port) - connection.connect(args.username, args.password) - - if args.publish: - connection.send(args.publish, **yaml.safe_load(args.data)) - - for topic in args.subscribe: - connection.subscribe(topic, callback_factory(topic)) - - if args.subscribe: - try: - while True: - pass - except KeyboardInterrupt: - print("exiting") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/TEM_comms/msgs/buffer.py b/TEM_comms/buffer.py similarity index 73% rename from TEM_comms/msgs/buffer.py rename to TEM_comms/buffer.py index 077218a..6870389 100644 --- a/TEM_comms/msgs/buffer.py +++ b/TEM_comms/buffer.py @@ -1,4 +1,4 @@ -from .base import BaseMessage +from emarfarap import BaseMessage class Status(BaseMessage): diff --git a/TEM_comms/msgs/camera.py b/TEM_comms/camera.py similarity index 92% rename from TEM_comms/msgs/camera.py rename to TEM_comms/camera.py index c38655c..a9b1984 100644 --- a/TEM_comms/msgs/camera.py +++ b/TEM_comms/camera.py @@ -1,4 +1,4 @@ -from .base import BaseMessage +from emarfarap import BaseMessage class Command(BaseMessage): tile_id: str diff --git a/TEM_comms/client.py b/TEM_comms/client.py deleted file mode 100644 index d3cf674..0000000 --- a/TEM_comms/client.py +++ /dev/null @@ -1,139 +0,0 @@ -import logging -import time - -import stomp -from typing import Callable, Dict, List -from stomp.utils import Frame -import stomp.exception -import importlib.metadata - -from . import exceptions - - -__version__ = importlib.metadata.version("TEM_comms") - - -class TEMComms: - def __init__( - self, - service: str, - host: str = "127.0.0.1", - port: int = 61616, - topics: Dict[str, Callable] = None, - logger: logging.Logger = None, - ): - self._service = service - self._connection = stomp.Connection12([(host, port)], heartbeats=(10000, 10000)) - self._topics = topics if topics is not None else {} - self._callbacks: Dict[str, Callable] = {} - self._connection.set_listener("listener", TEMCommsListener(self._handle_message)) - self._logger = logger if logger is not None else self._configure_logging() - - @staticmethod - def _configure_logging() -> logging.Logger: - logger = logging.getLogger(__name__) - logger.setLevel(logging.INFO) - return logger - - def connect( - self, - username: str = None, - password: str = None, - retry_limit: int = 8, - ): - """ - Connects to the STOMP server using the provided username and password. - - Args: - username (str, optional): The username to authenticate with. Defaults to None. - password (str, optional): The password to authenticate with. Defaults to None. - - Raises: - stomp.exception.ConnectFailedException: If the connection to the server fails. - - """ - retries = 0 - while retries < retry_limit: - try: - self._connection.connect( - username=username, password=password, wait=True - ) - self._logger.info("Connected to STOMP server.") - break - except stomp.exception.ConnectFailedException as e: - self._logger.error(f"Connection failed: {e}. Attempting to reconnect.") - retries += 1 - time.sleep(1) - if retries == retry_limit: - raise stomp.exception.ConnectFailedException( - f"Could not connect to server: {e}" - ) from e - - def send(self, topic: str, **data): - """ - Sends data to the specified topic. - - Args: - topic (str): The topic to send the data to. - **data: Keyword arguments representing the data to be sent. - - Raises: - exceptions.NoSuchTopicException: If the specified topic is not defined. - - """ - self._ensure_topic_exists(topic) - serialized_data = self._topics[topic](**data).serialize() - headers = dict(service=self._service, version=__version__) - self._connection.send(destination=topic, body=serialized_data, headers=headers) - self._logger.debug(f"Sent data to {topic}: {serialized_data}") - - def _ensure_topic_exists(self, topic: str): - if topic not in self._topics: - raise exceptions.NoSuchTopicException(f"Topic {topic} not defined.") - - def _handle_message(self, message_frame: Frame): - if message_frame.headers.get("version") != __version__: - raise exceptions.VersionMismatchException - topic = message_frame.headers["subscription"] - if topic not in self._topics: - self._logger.warning(f"Received message for unregistered topic: {topic}") - return - message_data = self._topics[topic].deserialize(message_frame.body) - self._callbacks[topic](message_data) - - def subscribe(self, topic: str, callback: Callable): - """ - Subscribes to a topic and associates a callback function to handle incoming messages. - - Args: - topic (str): The topic to subscribe to. - callback (Callable): The callback function to handle incoming messages. - - Raises: - NoSuchTopicException: If the specified topic is not defined. - - """ - self._ensure_topic_exists(topic) - if topic not in self._callbacks: - self._connection.subscribe(destination=topic, id=topic) - self._callbacks[topic] = callback - self._logger.info(f"Subscribed to {topic} with {callback.__name__}.") - - def unsubscribe(self, topic: str): - self._ensure_topic_exists(topic) - self._connection.unsubscribe(id=topic) - self._logger.info(f"Unsubscribed from {topic}.") - del self._callbacks[topic] - - def disconnect(self): - if self._connection.is_connected(): - self._connection.disconnect() - self._logger.info("Disconnected from STOMP server.") - - -class TEMCommsListener(stomp.ConnectionListener): - def __init__(self, callback: Callable): - self.callback = callback - - def on_message(self, frame): - self.callback(frame) diff --git a/TEM_comms/exceptions.py b/TEM_comms/exceptions.py deleted file mode 100644 index 431483c..0000000 --- a/TEM_comms/exceptions.py +++ /dev/null @@ -1,5 +0,0 @@ -class NoSuchTopicException(Exception): - pass - -class VersionMismatchException(Exception): - pass diff --git a/TEM_comms/logging.py b/TEM_comms/logging.py deleted file mode 100644 index acca33d..0000000 --- a/TEM_comms/logging.py +++ /dev/null @@ -1,13 +0,0 @@ -import logging - - -def setup_logging(logger_name: str, log_level: int = logging.INFO): - logger = logging.getLogger(logger_name) - handler = logging.StreamHandler() - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - handler.setFormatter(formatter) - logger.addHandler(handler) - logger.setLevel(log_level) - return logger diff --git a/TEM_comms/msgs/__init__.py b/TEM_comms/msgs/__init__.py deleted file mode 100644 index 10aaa9e..0000000 --- a/TEM_comms/msgs/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from . import buffer -from . import camera -from . import scope -from . import stage -from . import tile - -topics = { - "buffer.status": buffer.Status, - "camera.command": camera.Command, - "camera.image": camera.Image, - "camera.settings": camera.Settings, - "camera.status": camera.Status, - "scope.command": scope.Command, - "scope.status": scope.Status, - "stage.aperture.command": stage.aperture.Command, - "stage.aperture.status": stage.aperture.Status, - "stage.motion.command": stage.motion.Command, - "stage.motion.status": stage.motion.Status, - "stage.rotation.command": stage.rotation.Command, - "stage.rotation.status": stage.rotation.Status, - "tile.jpeg": tile.JPEG, - "tile.minimap": tile.Minimap, - "tile.processed": tile.Processed, - "tile.raw": tile.Raw, - "tile.statistics.focus": tile.statistics.Focus, - "tile.statistics.histogram": tile.statistics.Histogram, - "tile.statistics.min_max_mean": tile.statistics.MinMaxMean, - "tile.transform": tile.Transform, -} diff --git a/TEM_comms/msgs/base.py b/TEM_comms/msgs/base.py deleted file mode 100644 index 824cf04..0000000 --- a/TEM_comms/msgs/base.py +++ /dev/null @@ -1,12 +0,0 @@ -import pydantic - - -class BaseMessage(pydantic.BaseModel): - model_config = dict(extra="forbid") - - def serialize(self): - return self.model_dump_json() - - @classmethod - def deserialize(cls, data): - return cls.model_validate_json(data) diff --git a/TEM_comms/msgs/scope.py b/TEM_comms/scope.py similarity index 93% rename from TEM_comms/msgs/scope.py rename to TEM_comms/scope.py index 9ce5bc8..b124f3d 100644 --- a/TEM_comms/msgs/scope.py +++ b/TEM_comms/scope.py @@ -1,4 +1,4 @@ -from .base import BaseMessage +from emarfarap import BaseMessage from typing import Literal from pydantic import model_validator diff --git a/TEM_comms/msgs/stage/__init__.py b/TEM_comms/stage/__init__.py similarity index 100% rename from TEM_comms/msgs/stage/__init__.py rename to TEM_comms/stage/__init__.py diff --git a/TEM_comms/msgs/stage/aperture.py b/TEM_comms/stage/aperture.py similarity index 80% rename from TEM_comms/msgs/stage/aperture.py rename to TEM_comms/stage/aperture.py index c895783..6ce84e9 100644 --- a/TEM_comms/msgs/stage/aperture.py +++ b/TEM_comms/stage/aperture.py @@ -1,4 +1,4 @@ -from TEM_comms.msgs.base import BaseMessage +from emarfarap import BaseMessage class Command(BaseMessage): aperture_id: int | None = None diff --git a/TEM_comms/msgs/stage/motion.py b/TEM_comms/stage/motion.py similarity index 85% rename from TEM_comms/msgs/stage/motion.py rename to TEM_comms/stage/motion.py index 09dbc7f..4dd0274 100644 --- a/TEM_comms/msgs/stage/motion.py +++ b/TEM_comms/stage/motion.py @@ -1,4 +1,4 @@ -from ..base import BaseMessage +from emarfarap import BaseMessage class Command(BaseMessage): x: int | None = None diff --git a/TEM_comms/msgs/stage/rotation.py b/TEM_comms/stage/rotation.py similarity index 87% rename from TEM_comms/msgs/stage/rotation.py rename to TEM_comms/stage/rotation.py index 9804f28..151d21e 100644 --- a/TEM_comms/msgs/stage/rotation.py +++ b/TEM_comms/stage/rotation.py @@ -1,4 +1,4 @@ -from ..base import BaseMessage +from emarfarap import BaseMessage class Command(BaseMessage): angle_x: float | None = None diff --git a/TEM_comms/msgs/tile/__init__.py b/TEM_comms/tile/__init__.py similarity index 92% rename from TEM_comms/msgs/tile/__init__.py rename to TEM_comms/tile/__init__.py index 165648d..34e1ad5 100644 --- a/TEM_comms/msgs/tile/__init__.py +++ b/TEM_comms/tile/__init__.py @@ -1,4 +1,4 @@ -from ..base import BaseMessage +from emarfarap import BaseMessage from . import statistics diff --git a/TEM_comms/msgs/tile/statistics.py b/TEM_comms/tile/statistics.py similarity index 86% rename from TEM_comms/msgs/tile/statistics.py rename to TEM_comms/tile/statistics.py index 996d0e6..071ab1b 100644 --- a/TEM_comms/msgs/tile/statistics.py +++ b/TEM_comms/tile/statistics.py @@ -1,4 +1,4 @@ -from ..base import BaseMessage +from emarfarap import BaseMessage class Focus(BaseMessage): tile_id: str diff --git a/dev-requirements.in b/dev-requirements.in deleted file mode 100644 index 236e26a..0000000 --- a/dev-requirements.in +++ /dev/null @@ -1,4 +0,0 @@ -pytest -mypy -uv -ruff \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 220f78a..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile dev-requirements.in -o dev-requirements.txt -iniconfig==2.0.0 - # via pytest -mypy==1.10.0 -mypy-extensions==1.0.0 - # via mypy -packaging==24.0 - # via pytest -pluggy==1.5.0 - # via pytest -pytest==8.2.0 -ruff==0.4.2 -typing-extensions==4.11.0 - # via mypy -uv==0.1.39 diff --git a/dev.env b/dev.env deleted file mode 100644 index 91c2316..0000000 --- a/dev.env +++ /dev/null @@ -1,3 +0,0 @@ -ARTEMIS_HOST=artemis -ARTEMIS_PORT=61613 -ANONYMOUS_LOGIN=true \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 33d7b4b..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -services: - artemis: - image: apache/activemq-artemis - env_file: - - dev.env - ports: - - 8161:8161 # Web Console - - 61613:61613 # STOMP - volumes: - - data:/var/lib/artemis/data - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8161/health"] - interval: 30s - timeout: 10s - retries: 5 - - - publisher: - build: - context: . - dockerfile: Dockerfile.publisher - depends_on: - - artemis - env_file: - - dev.env - - subscriber: - build: - context: . - dockerfile: Dockerfile.subscriber - depends_on: - - artemis - env_file: - - dev.env - -volumes: - data: \ No newline at end of file diff --git a/examples/publisher.py b/examples/publisher.py deleted file mode 100644 index 096a0ee..0000000 --- a/examples/publisher.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import time - -from TEM_comms.client import TEMComms -from TEM_comms.logging import setup_logging -from TEM_comms.msgs import topics - -logger = setup_logging("publisher") - -host = os.environ.get("ARTEMIS_HOST", "127.0.0.1") -port = int(os.environ.get("ARTEMIS_PORT", 61613)) - -connection = TEMComms("Publisher", host=host, port=port, topics=topics, logger=logger) -connection.connect(username="admin", password="password") - -while True: - connection.send("tile.statistics.focus", tile_id="test", focus=11.2) - time.sleep(1) diff --git a/examples/server.sh b/examples/server.sh deleted file mode 100755 index cc183d4..0000000 --- a/examples/server.sh +++ /dev/null @@ -1 +0,0 @@ -sudo docker run -t --rm -p 61616:61616 --env ANONYMOUS_LOGIN=true apache/activemq-artemis \ No newline at end of file diff --git a/examples/subscriber.py b/examples/subscriber.py deleted file mode 100644 index b0fb85a..0000000 --- a/examples/subscriber.py +++ /dev/null @@ -1,22 +0,0 @@ -import os - -from TEM_comms.client import TEMComms -from TEM_comms.msgs import topics -from TEM_comms.logging import setup_logging - -logger = setup_logging("subscriber") - -host = os.environ.get("ARTEMIS_HOST", "127.0.0.1") -port = int(os.environ.get("ARTEMIS_PORT", 61613)) - - -def handle_focus_message(message): - logger.info(f"Received focus message: {message}") - - -connection = TEMComms("Subscriber", host=host, port=port, topics=topics, logger=logger) -connection.connect(username="admin", password="password") -connection.subscribe("tile.statistics.focus", handle_focus_message) - -while True: - pass diff --git a/pyproject.toml b/pyproject.toml index a8c02ae..7816710 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,16 @@ [project] -name = "TEM_comms" +name = "pigeon-tem-comms" version = "0.1.0" authors = [ { name="Cameron Devine", email="cameron.devine@alleninstitute.org" }, ] -description = "The communication library for the next generation TEM system." +description = "Pigeon messages for the next generation TEM imaging system." readme = "README.md" requires-python = ">=3.10" -dynamic = ["dependencies"] - -[tool.setuptools.dynamic] -dependencies = {file = ["requirements.txt"]} -optional-dependencies = {dev = { file = ["dev-requirements.txt"] }} - -[project.scripts] -TEM_comm_cli = "TEM_comms.__main__:main" +dependencies = [ + "pigeon-client", + "pydantic", +] -[project.urls] -Homepage = "https://github.com/AllenInstitute/TEM_comms" -Issues = "https://github.com/AllenInstitute/TEM_comms/issues" \ No newline at end of file +[project.entry-points."pigeon.msgs"] +msgs = "TEM_comms:msgs" \ No newline at end of file diff --git a/requirements.in b/requirements.in deleted file mode 100644 index bb1d100..0000000 --- a/requirements.in +++ /dev/null @@ -1,3 +0,0 @@ -stomp-py -pydantic -pyyaml \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f7f795a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile requirements.in -o requirements.txt -annotated-types==0.6.0 - # via pydantic -docopt==0.6.2 - # via stomp-py -pydantic==2.7.1 -pydantic-core==2.18.2 - # via pydantic -pyyaml==6.0.1 -stomp-py==8.1.2 -typing-extensions==4.11.0 - # via - # pydantic - # pydantic-core -websocket-client==1.8.0 - # via stomp-py diff --git a/tests/test_base_message.py b/tests/test_base_message.py deleted file mode 100644 index 555fa1c..0000000 --- a/tests/test_base_message.py +++ /dev/null @@ -1,38 +0,0 @@ -from TEM_comms.msgs.base import BaseMessage -import json -from pydantic import ValidationError -import pytest - - -class Msg(BaseMessage): - x: int - y: int - - -def test_serialize(): - data = {"x": 1, "y": 2} - msg = Msg(**data) - - assert json.loads(msg.serialize()) == data - - -def test_deserialize(): - data = {"x": 3, "y": 4} - msg = Msg.deserialize(json.dumps(data)) - - assert msg.x == data["x"] - assert msg.y == data["y"] - - -def test_forbid_extra(): - with pytest.raises(ValidationError): - Msg(x=1, y=2, z=3) - with pytest.raises(ValidationError): - Msg.deserialize(json.dumps({"x": 1, "y": 2, "z": 3})) - - -def test_mising_values(): - with pytest.raises(ValidationError): - Msg(x=1) - with pytest.raises(ValidationError): - Msg.deserialize(json.dumps({"y": 3})) diff --git a/tests/test_buffer.py b/tests/test_buffer.py index f63ee0c..6c8ad5c 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -1,7 +1,7 @@ import re import pytest -from TEM_comms.msgs.buffer import Status +from TEM_comms.buffer import Status from pydantic import ValidationError diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index 6b39257..0000000 --- a/tests/test_client.py +++ /dev/null @@ -1,177 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch -from stomp.exception import ConnectFailedException -from TEM_comms.client import TEMComms, __version__ -from TEM_comms.exceptions import NoSuchTopicException -from TEM_comms.msgs.base import BaseMessage - - -class MockMessage(BaseMessage): - field1: str - - -@pytest.fixture -def tem_comm_client(): - with patch("TEM_comms.logging.setup_logging") as mock_logging: - topics = {"topic1": MockMessage} - yield TEMComms( - "test", host="localhost", port=61613, logger=mock_logging.Logger(), topics=topics - ) - - -@pytest.mark.parametrize( - "username, password, expected_log", - [ - (None, None, "Connected to STOMP server."), - ("user", "pass", "Connected to STOMP server."), - ], - ids=["no-auth", "with-auth"], -) -def test_connect(tem_comm_client, username, password, expected_log): - # Arrange - tem_comm_client._connection.connect = MagicMock() - - # Act - tem_comm_client.connect(username=username, password=password) - - # Assert - tem_comm_client._connection.connect.assert_called_with( - username=username, password=password, wait=True - ) - tem_comm_client._logger.info.assert_called_with(expected_log) - - -@pytest.mark.parametrize( - "username, password", - [ - (None, None), - ("user", "pass"), - ], - ids=["connect-fail-no-auth", "connect-fail-with-auth"], -) -def test_connect_failure(tem_comm_client, username, password): - # Arrange - tem_comm_client._connection.connect = MagicMock( - side_effect=ConnectFailedException("Connection failed") - ) - retry_limit = 1 - tem_comm_client._logger.error = MagicMock() - - # Act & Assert - with pytest.raises( - ConnectFailedException, match="Could not connect to server: Connection failed" - ): - tem_comm_client.connect(username=username, password=password, retry_limit=retry_limit) - - # Assert the logger was called the same number of times as the retry limit - assert tem_comm_client._logger.error.call_count == retry_limit - - -@pytest.mark.parametrize( - "topic, data, expected_serialized_data", - [ - ("topic1", {"field1": "value"}, '{"field1":"value"}'), - ], - ids=["send-data"], -) -def test_send(tem_comm_client, topic, data, expected_serialized_data): - - - expected_headers = {"service": "test", "version": __version__} - # Arrange - tem_comm_client._topics[topic] = MockMessage - tem_comm_client._connection.send = MagicMock() - - # Act - tem_comm_client.send(topic, **data) - - # Assert - tem_comm_client._connection.send.assert_called_with( - destination=topic, body=expected_serialized_data, headers=expected_headers - ) - tem_comm_client._logger.debug.assert_called_with( - f"Sent data to {topic}: {expected_serialized_data}" - ) -@pytest.mark.parametrize( - "topic, data", - [ - ("unknown_topic", {"key": "value"}), - ], - ids=["send-data-no-such-topic"], -) -def test_send_no_such_topic(tem_comm_client, topic, data): - # Act & Assert - with pytest.raises(NoSuchTopicException, match=f"Topic {topic} not defined."): - tem_comm_client.send(topic, **data) - - -@pytest.mark.parametrize( - "topic, callback_name, expected_log", - [ - ("topic1", "callback", "Subscribed to topic1 with callback."), - ], - ids=["subscribe-new-topic"], -) -def test_subscribe(tem_comm_client, topic, callback_name, expected_log): - # Arrange - tem_comm_client._topics[topic] = MockMessage - callback = MagicMock(__name__=callback_name) - tem_comm_client._connection.subscribe = MagicMock() - - # Act - tem_comm_client.subscribe(topic, callback) - - # Assert - assert tem_comm_client._callbacks[topic] == callback - tem_comm_client._connection.subscribe.assert_called_with(destination=topic, id=topic) - tem_comm_client._logger.info.assert_called_with(expected_log) - - -@pytest.mark.parametrize( - "topic", - [ - ("unknown_topic"), - ], - ids=["subscribe-no-such-topic"], -) -def test_subscribe_no_such_topic(tem_comm_client, topic): - # Arrange - callback = MagicMock() - - # Act & Assert - with pytest.raises(NoSuchTopicException, match=f"Topic {topic} not defined."): - tem_comm_client.subscribe(topic, callback) - - -@pytest.mark.parametrize( - "topic, expected_log", - [ - ("topic1", "Unsubscribed from topic1."), - ], - ids=["unsubscribe-existing-topic"], -) -def test_unsubscribe(tem_comm_client, topic, expected_log): - # Arrange - tem_comm_client._callbacks[topic] = ["topic1"] - tem_comm_client._connection.unsubscribe = MagicMock() - - # Act - tem_comm_client.unsubscribe(topic) - - # Assert - assert topic not in tem_comm_client._callbacks - tem_comm_client._connection.unsubscribe.assert_called_with(id=topic) - tem_comm_client._logger.info.assert_called_with(expected_log) - - -def test_disconnect(tem_comm_client): - # Arrange - tem_comm_client._connection.is_connected = MagicMock(return_value=True) - tem_comm_client._connection.disconnect = MagicMock() - - # Act - tem_comm_client.disconnect() - - # Assert - tem_comm_client._connection.disconnect.assert_called_once() - tem_comm_client._logger.info.assert_called_with("Disconnected from STOMP server.") diff --git a/test/test_scope_command.py b/tests/test_scope_command.py similarity index 91% rename from test/test_scope_command.py rename to tests/test_scope_command.py index 23f45cc..b699d27 100644 --- a/test/test_scope_command.py +++ b/tests/test_scope_command.py @@ -1,4 +1,4 @@ -from TEM_comms.msgs.scope import Command +from TEM_comms.scope import Command from pydantic import ValidationError import pytest