ezmsg-baseproc 1.0.1__tar.gz → 1.0.3__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.
Files changed (39) hide show
  1. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/PKG-INFO +3 -3
  2. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/README.md +2 -2
  3. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/__version__.py +2 -2
  4. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/protocols.py +3 -1
  5. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/util/profile.py +20 -11
  6. ezmsg_baseproc-1.0.3/tests/test_profile.py +189 -0
  7. ezmsg_baseproc-1.0.1/examples/example.py +0 -44
  8. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/.github/workflows/docs.yml +0 -0
  9. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/.github/workflows/python-publish.yml +0 -0
  10. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/.github/workflows/python-tests.yml +0 -0
  11. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/.gitignore +0 -0
  12. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/.pre-commit-config.yaml +0 -0
  13. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/LICENSE +0 -0
  14. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/Makefile +0 -0
  15. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/make.bat +0 -0
  16. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/_templates/autosummary/module.rst +0 -0
  17. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/api/index.rst +0 -0
  18. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/conf.py +0 -0
  19. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/guides/ProcessorsBase.md +0 -0
  20. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/guides/how-tos/processors/adaptive.rst +0 -0
  21. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/guides/how-tos/processors/checkpoint.rst +0 -0
  22. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/guides/how-tos/processors/composite.rst +0 -0
  23. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/guides/how-tos/processors/content-processors.rst +0 -0
  24. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/guides/how-tos/processors/processor.rst +0 -0
  25. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/guides/how-tos/processors/standalone.rst +0 -0
  26. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/guides/how-tos/processors/stateful.rst +0 -0
  27. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/guides/how-tos/processors/unit.rst +0 -0
  28. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/docs/source/index.rst +0 -0
  29. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/pyproject.toml +0 -0
  30. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/__init__.py +0 -0
  31. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/composite.py +0 -0
  32. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/processor.py +0 -0
  33. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/stateful.py +0 -0
  34. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/units.py +0 -0
  35. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/util/__init__.py +0 -0
  36. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/util/asio.py +0 -0
  37. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/util/message.py +0 -0
  38. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/src/ezmsg/baseproc/util/typeresolution.py +0 -0
  39. {ezmsg_baseproc-1.0.1 → ezmsg_baseproc-1.0.3}/tests/test_baseproc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ezmsg-baseproc
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Base processor classes and protocols for ezmsg signal processing pipelines
5
5
  Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>, Kyle McGraw <kmcgraw@blackrockneuro.com>
6
6
  License-Expression: MIT
@@ -52,7 +52,7 @@ ezmsg.baseproc/
52
52
  ```python
53
53
  from dataclasses import dataclass
54
54
  from ezmsg.baseproc import BaseTransformer
55
- from ezmsg.util.messages.axisarray import AxisArray
55
+ from ezmsg.util.messages.axisarray import AxisArray, replace
56
56
 
57
57
  @dataclass
58
58
  class MySettings:
@@ -60,7 +60,7 @@ class MySettings:
60
60
 
61
61
  class MyTransformer(BaseTransformer[MySettings, AxisArray, AxisArray]):
62
62
  def _process(self, message: AxisArray) -> AxisArray:
63
- return message.replace(data=message.data * self.settings.scale)
63
+ return replace(message, data=message.data * self.settings.scale)
64
64
  ```
65
65
 
66
66
  ### Creating a Stateful Transformer
@@ -40,7 +40,7 @@ ezmsg.baseproc/
40
40
  ```python
41
41
  from dataclasses import dataclass
42
42
  from ezmsg.baseproc import BaseTransformer
43
- from ezmsg.util.messages.axisarray import AxisArray
43
+ from ezmsg.util.messages.axisarray import AxisArray, replace
44
44
 
45
45
  @dataclass
46
46
  class MySettings:
@@ -48,7 +48,7 @@ class MySettings:
48
48
 
49
49
  class MyTransformer(BaseTransformer[MySettings, AxisArray, AxisArray]):
50
50
  def _process(self, message: AxisArray) -> AxisArray:
51
- return message.replace(data=message.data * self.settings.scale)
51
+ return replace(message, data=message.data * self.settings.scale)
52
52
  ```
53
53
 
54
54
  ### Creating a Stateful Transformer
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.0.1'
32
- __version_tuple__ = version_tuple = (1, 0, 1)
31
+ __version__ = version = '1.0.3'
32
+ __version_tuple__ = version_tuple = (1, 0, 3)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -4,6 +4,8 @@ import functools
4
4
  import typing
5
5
  from dataclasses import dataclass
6
6
 
7
+ import ezmsg.core as ez
8
+
7
9
  from .util.message import SampleMessage
8
10
 
9
11
  # --- Processor state decorator ---
@@ -12,7 +14,7 @@ processor_state = functools.partial(dataclass, unsafe_hash=True, frozen=False, i
12
14
  # --- Type variables for protocols and processors ---
13
15
  MessageInType = typing.TypeVar("MessageInType")
14
16
  MessageOutType = typing.TypeVar("MessageOutType")
15
- SettingsType = typing.TypeVar("SettingsType")
17
+ SettingsType = typing.TypeVar("SettingsType", bound=ez.Settings)
16
18
  StateType = typing.TypeVar("StateType")
17
19
 
18
20
 
@@ -31,20 +31,29 @@ def _setup_logger(append: bool = False) -> logging.Logger:
31
31
  write_header = True
32
32
  if logpath.exists() and logpath.is_file():
33
33
  if append:
34
- with open(logpath) as f:
35
- first_line = f.readline().rstrip()
36
- if first_line == HEADER:
34
+ try:
35
+ with open(logpath) as f:
36
+ first_line = f.readline().rstrip()
37
+ if first_line == HEADER:
38
+ write_header = False
39
+ else:
40
+ # Remove the file if appending, but headers do not match
41
+ ezmsg_logger = logging.getLogger("ezmsg")
42
+ ezmsg_logger.warning(
43
+ "Profiling header mismatch: please make sure to use the same version of "
44
+ "ezmsg for all processes."
45
+ )
46
+ logpath.unlink()
47
+ except (PermissionError, OSError):
48
+ # On Windows, file may be locked by another process - just append
37
49
  write_header = False
38
- else:
39
- # Remove the file if appending, but headers do not match
40
- ezmsg_logger = logging.getLogger("ezmsg")
41
- ezmsg_logger.warning(
42
- "Profiling header mismatch: please make sure to use the same version of ezmsg for all processes."
43
- )
44
- logpath.unlink()
45
50
  else:
46
51
  # Remove the file if not appending
47
- logpath.unlink()
52
+ try:
53
+ logpath.unlink()
54
+ except (PermissionError, OSError):
55
+ # On Windows, file may be locked by another process - continue anyway
56
+ pass
48
57
 
49
58
  # Create a logger with the name "ezprofile"
50
59
  _logger = logging.getLogger("ezprofile")
@@ -0,0 +1,189 @@
1
+ import logging
2
+ import os
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+
9
+ from ezmsg.baseproc.util.profile import (
10
+ HEADER,
11
+ _setup_logger,
12
+ get_logger_path,
13
+ profile_method,
14
+ profile_subpub,
15
+ )
16
+
17
+
18
+ def test_logger_creation():
19
+ """Test that the ezprofile logger is created when importing from ezmsg.baseproc.util.profile."""
20
+ logger_name = "ezprofile"
21
+ assert logger_name in logging.Logger.manager.loggerDict
22
+ logger = logging.getLogger(logger_name)
23
+ assert logger.level == logging.INFO
24
+ assert len(logger.handlers) == 1
25
+ handler = logger.handlers[0]
26
+ assert isinstance(handler, logging.FileHandler)
27
+ assert handler.level == logging.DEBUG
28
+ assert Path(handler.baseFilename) == get_logger_path()
29
+
30
+ # Remove and close all handlers
31
+ logger.removeHandler(handler)
32
+ handler.close()
33
+
34
+
35
+ @pytest.fixture
36
+ def mock_env():
37
+ """Fixture to mock environment variables."""
38
+ with patch.dict(os.environ, {"EZMSG_PROFILE": "test_profiler.log", "EZMSG_LOGLEVEL": "DEBUG"}):
39
+ yield
40
+
41
+
42
+ @pytest.fixture
43
+ def mock_logger_path(mock_env):
44
+ """Fixture to mock the logger path."""
45
+ logpath = get_logger_path()
46
+ yield logpath
47
+
48
+
49
+ def test_logger_not_append(mock_logger_path):
50
+ """Test that the logger creates a new file if append==False."""
51
+ some_text = "Some,Previous,Logger,Text"
52
+ correct_header = HEADER
53
+
54
+ # Create a file with some text
55
+ test_logpath = mock_logger_path
56
+ with open(test_logpath, "w") as f:
57
+ f.truncate(0)
58
+ f.write(some_text + "\n")
59
+
60
+ logger = _setup_logger(append=False)
61
+ assert mock_logger_path.exists() and mock_logger_path.is_file()
62
+ assert logger.name == "ezprofile"
63
+ assert logger.level == logging.DEBUG
64
+ assert len(logger.handlers) == 1
65
+ handler = logger.handlers[0]
66
+ assert isinstance(handler, logging.FileHandler)
67
+ assert handler.level == logging.DEBUG
68
+ assert Path(handler.baseFilename) == test_logpath
69
+
70
+ with open(mock_logger_path, "r") as f:
71
+ first_line = f.readline().strip()
72
+ assert first_line == correct_header
73
+
74
+ # Clean up the logger file handler
75
+ logger.removeHandler(handler)
76
+ handler.close()
77
+
78
+
79
+ def test_logger_append_header_mismatch(mock_logger_path):
80
+ """Test that the logger deletes the file if the header mismatches when append=True."""
81
+ incorrect_header = "Incorrect,Header,Format"
82
+ correct_header = HEADER
83
+
84
+ # Create a file with an incorrect header
85
+ test_logpath = mock_logger_path
86
+ with open(test_logpath, "w") as f:
87
+ f.truncate(0)
88
+ f.write(incorrect_header + "\n")
89
+
90
+ logger = _setup_logger(append=True)
91
+
92
+ # Assert that the file was deleted and recreated
93
+ assert test_logpath.exists()
94
+ with open(test_logpath, "r") as f:
95
+ first_line = f.readline().strip()
96
+ assert first_line == correct_header
97
+
98
+ # Clean up the logger handlers
99
+ handlers = logger.handlers
100
+ for handler in handlers:
101
+ logger.removeHandler(handler)
102
+ handler.close()
103
+
104
+
105
+ def test_logger_append_header_match(mock_logger_path):
106
+ """Test that the logger correctly appends to the logfile if append=True and the header is correct."""
107
+ correct_header = HEADER
108
+ next_line = "Some,New,Logger,Text"
109
+
110
+ # Create a file with the correct header
111
+ test_logpath = mock_logger_path
112
+ with open(test_logpath, "w") as f:
113
+ f.truncate(0)
114
+ f.write(correct_header + "\n")
115
+ f.write(next_line + "\n")
116
+
117
+ logger = _setup_logger(append=True)
118
+ logger.debug("Some,Added,Logger,Text")
119
+
120
+ # Assert that the file was appended to
121
+ assert test_logpath.exists()
122
+ with open(test_logpath, "r") as f:
123
+ first_line = f.readline().strip()
124
+ second_line = f.readline().strip()
125
+ third_line = f.readline().strip()
126
+ assert first_line == correct_header
127
+ assert second_line == next_line
128
+ assert "Some,Added,Logger,Text" in third_line
129
+
130
+ # Clean up the logger handlers
131
+ handlers = logger.handlers
132
+ for handler in handlers:
133
+ if isinstance(handler, logging.FileHandler):
134
+ logger.removeHandler(handler)
135
+ handler.close()
136
+
137
+
138
+ def test_profile_method_decorator():
139
+ """Test the profile_method decorator."""
140
+
141
+ class DummyClass:
142
+ address = "dummy_address"
143
+
144
+ @profile_method(trace_oldest=True)
145
+ def sample_method(self, x, y):
146
+ return x + y
147
+
148
+ instance = DummyClass()
149
+
150
+ with patch("ezmsg.baseproc.util.profile.logger") as mock_logger:
151
+ result = instance.sample_method(2, 3)
152
+ assert result == 5
153
+
154
+ # Assert the logger was called
155
+ mock_logger.debug.assert_called()
156
+
157
+
158
+ @pytest.mark.asyncio
159
+ async def test_profile_subpub_decorator(mock_logger_path):
160
+ """Test the profile_subpub decorator."""
161
+ test_logpath = mock_logger_path
162
+ _setup_logger()
163
+
164
+ class DummyUnit:
165
+ address = "dummy_address"
166
+
167
+ @profile_subpub(trace_oldest=True)
168
+ async def sample_subpub(self, msg):
169
+ yield "stream", msg
170
+
171
+ unit = DummyUnit()
172
+
173
+ async for stream, obj in unit.sample_subpub("message"):
174
+ # Assert the generator yields correctly
175
+ assert stream == "stream"
176
+ assert obj == "message"
177
+
178
+ # Assert the logger was called and printed in the correct format
179
+ with open(test_logpath, "r") as f:
180
+ f.readline()
181
+ second_line = f.readline().strip()
182
+ log_text = second_line.split(",")
183
+ assert len(log_text) == 6
184
+ datetime.strptime(log_text[0], "%Y-%m-%dT%H:%M:%S%z") # Will throw if format is incorrect
185
+ assert log_text[1].endswith("test_profile.DummyUnit")
186
+ assert log_text[2] == "dummy_address"
187
+ assert log_text[3] == "None"
188
+ float(log_text[4]) # Will throw if not float
189
+ float(log_text[5]) # Will throw if not float
@@ -1,44 +0,0 @@
1
- """Example usage of ezmsg-baseproc package."""
2
-
3
- import asyncio
4
-
5
- import ezmsg.core as ez
6
-
7
- # Import your units from the package
8
- # from ezmsg.baseproc import MyUnit
9
-
10
-
11
- class ExampleSettings(ez.Settings):
12
- """Settings for ExampleUnit."""
13
-
14
- message: str = "Hello from ezmsg-baseproc!"
15
-
16
-
17
- class ExampleUnit(ez.Unit):
18
- """Example ezmsg Unit demonstrating basic patterns."""
19
-
20
- SETTINGS = ExampleSettings
21
-
22
- INPUT = ez.InputStream(str)
23
- OUTPUT = ez.OutputStream(str)
24
-
25
- @ez.subscriber(INPUT)
26
- @ez.publisher(OUTPUT)
27
- async def on_message(self, message: str) -> ez.AsyncGenerator:
28
- """Process incoming messages."""
29
- result = f"{self.SETTINGS.message} Received: {message}"
30
- yield self.OUTPUT, result
31
-
32
-
33
- async def main():
34
- """Run the example."""
35
- print("ezmsg-baseproc loaded successfully!")
36
- print(f"Version: {__import__('ezmsg.baseproc').__version__}")
37
-
38
- # Example: Create and run a simple system
39
- # system = ExampleSystem()
40
- # await ez.run(SYSTEM=system)
41
-
42
-
43
- if __name__ == "__main__":
44
- asyncio.run(main())
File without changes