arroyopy 0.1.0__py3-none-any.whl
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.
- arroyopy/__init__.py +0 -0
- arroyopy/listener.py +14 -0
- arroyopy/operator.py +52 -0
- arroyopy/publisher.py +12 -0
- arroyopy/redis.py +62 -0
- arroyopy/schemas.py +67 -0
- arroyopy/timing.py +54 -0
- arroyopy/zmq.py +60 -0
- arroyopy-0.1.0.dist-info/METADATA +255 -0
- arroyopy-0.1.0.dist-info/RECORD +12 -0
- arroyopy-0.1.0.dist-info/WHEEL +4 -0
- arroyopy-0.1.0.dist-info/licenses/LICENSE +33 -0
arroyopy/__init__.py
ADDED
|
File without changes
|
arroyopy/listener.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Listener(ABC):
|
|
6
|
+
message_queue: asyncio.Queue
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
async def start(self, message_queue) -> None:
|
|
10
|
+
self.message_queue = message_queue
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
async def stop(self) -> None:
|
|
14
|
+
pass
|
arroyopy/operator.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from .listener import Listener
|
|
7
|
+
from .publisher import Publisher
|
|
8
|
+
from .schemas import Message
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Operator(ABC):
|
|
14
|
+
listeners: List[Listener] = []
|
|
15
|
+
publishers: List[Publisher] = []
|
|
16
|
+
stop_requested: bool = False
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.listener_queue = asyncio.Queue()
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
async def process(self, message: Message) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
async def add_listener(self, listener: Listener) -> None: # noqa
|
|
26
|
+
self.listeners.append(listener)
|
|
27
|
+
await listener.start(self.listener_queue)
|
|
28
|
+
|
|
29
|
+
def remove_listener(self, listener: Listener) -> None: # noqa
|
|
30
|
+
self.listeners.remove(listener)
|
|
31
|
+
|
|
32
|
+
def add_publisher(self, publisher: Publisher) -> None:
|
|
33
|
+
self.publishers.append(publisher)
|
|
34
|
+
|
|
35
|
+
def remove_publisher(self, publisher: Publisher) -> None:
|
|
36
|
+
self.publishers.remove(publisher)
|
|
37
|
+
|
|
38
|
+
async def publish(self, message: Message) -> None:
|
|
39
|
+
for publisher in self.publishers:
|
|
40
|
+
await publisher.publish(message)
|
|
41
|
+
|
|
42
|
+
async def start(self):
|
|
43
|
+
# Process messages from the queue
|
|
44
|
+
while True:
|
|
45
|
+
if self.stop_requested:
|
|
46
|
+
logger.info("Stopping operator...")
|
|
47
|
+
for listener in self.listeners:
|
|
48
|
+
await listener.stop()
|
|
49
|
+
break
|
|
50
|
+
message = await self.queue.get()
|
|
51
|
+
processed_message = await self.process(message)
|
|
52
|
+
await self.publish(processed_message)
|
arroyopy/publisher.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Generic, TypeVar
|
|
3
|
+
|
|
4
|
+
from .schemas import Message
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T", bound=Message)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Publisher(ABC, Generic[T]):
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def publish(self, message: Message) -> None:
|
|
12
|
+
pass
|
arroyopy/redis.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from redis.asyncio.client import Redis
|
|
4
|
+
|
|
5
|
+
from .listener import Listener
|
|
6
|
+
from .operator import Operator
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("arroyo.zmq")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RedisListener(Listener):
|
|
12
|
+
def __init__(
|
|
13
|
+
self, redis_client: Redis, redis_channel_name: str, operator: Operator
|
|
14
|
+
):
|
|
15
|
+
self.stop_requested = False
|
|
16
|
+
self.redis_client: Redis = redis_client
|
|
17
|
+
self.redis_channel_name = redis_channel_name
|
|
18
|
+
self.operator = operator
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
async def from_client(
|
|
22
|
+
cls, redis_client: Redis, redis_channel_name: str, operator: Operator
|
|
23
|
+
):
|
|
24
|
+
return RedisListener(redis_client, redis_channel_name, operator)
|
|
25
|
+
|
|
26
|
+
async def start(self):
|
|
27
|
+
logger.info("Listener started")
|
|
28
|
+
pubsub = self.redis_client.pubsub()
|
|
29
|
+
await pubsub.subscribe(self.redis_channel_name)
|
|
30
|
+
# Listen for messages in the subscribed channel
|
|
31
|
+
while True:
|
|
32
|
+
if self.stop_requested:
|
|
33
|
+
return
|
|
34
|
+
# get_message blocks until timeout, returning None if no message in that time
|
|
35
|
+
raw_msg = await pubsub.get_message(
|
|
36
|
+
ignore_subscribe_messages=True, timeout=1.0
|
|
37
|
+
)
|
|
38
|
+
if raw_msg is None:
|
|
39
|
+
continue
|
|
40
|
+
msg = raw_msg["data"]
|
|
41
|
+
if logger.getEffectiveLevel() == logging.DEBUG:
|
|
42
|
+
logger.debug(f"{msg=}")
|
|
43
|
+
await self.operator.process(msg)
|
|
44
|
+
|
|
45
|
+
async def stop(self):
|
|
46
|
+
self.stop_requested = True
|
|
47
|
+
await self.redis_client.aclose()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RedisPublisher:
|
|
51
|
+
def __init__(self, redis_client: Redis, redis_channel_name: str):
|
|
52
|
+
self.redis_client: Redis = redis_client
|
|
53
|
+
self.redis_channel_name = redis_channel_name
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
async def from_client(cls, redis_client: Redis, redis_channel_name: str):
|
|
57
|
+
return RedisPublisher(redis_client, redis_channel_name)
|
|
58
|
+
|
|
59
|
+
async def publish(self, message):
|
|
60
|
+
await self.redis_client.publish(self.redis_channel_name, message)
|
|
61
|
+
if logger.getEffectiveLevel() == logging.DEBUG:
|
|
62
|
+
logger.debug(f"Published {message=}")
|
arroyopy/schemas.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
import numpy.typing
|
|
3
|
+
import pandas
|
|
4
|
+
from pydantic import BaseModel, field_validator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Message:
|
|
8
|
+
""" "
|
|
9
|
+
Base class for messages. Is not a pydantic model
|
|
10
|
+
in case implementations choose not to use pydantic
|
|
11
|
+
as a validation and (de)serialization system but still
|
|
12
|
+
want to indicate that they pass arroyo Messages.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PydanticMessage(Message, BaseModel):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Start(PydanticMessage):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Stop(PydanticMessage):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Event(PydanticMessage):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DataFrameModel(BaseModel):
|
|
35
|
+
"""
|
|
36
|
+
A Pydantic model for validating pd.DataFrame objects.
|
|
37
|
+
Does not parse array, merely validates that is a pd.DataFrame
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
df: pandas.DataFrame
|
|
41
|
+
|
|
42
|
+
@field_validator("df", mode="before")
|
|
43
|
+
def validate_is_numpy_array(cls, v):
|
|
44
|
+
if not isinstance(v, pandas.DataFrame):
|
|
45
|
+
raise TypeError(f"Expected pd.DataFrame, got {type(v)} instead.")
|
|
46
|
+
return v # Do not modify or parse the array
|
|
47
|
+
|
|
48
|
+
class Config:
|
|
49
|
+
arbitrary_types_allowed = True # Allow numpy.ndarray type
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NumpyArrayModel(BaseModel):
|
|
53
|
+
"""
|
|
54
|
+
A Pydantic model for validating numpy.ndarray objects.
|
|
55
|
+
Does not parse array, merely validates that is a np.ndarray
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
array: numpy.ndarray
|
|
59
|
+
|
|
60
|
+
@field_validator("array", mode="before")
|
|
61
|
+
def validate_is_numpy_array(cls, v):
|
|
62
|
+
if not isinstance(v, numpy.ndarray):
|
|
63
|
+
raise TypeError(f"Expected numpy.ndarray, got {type(v)} instead.")
|
|
64
|
+
return v # Do not modify or parse the array
|
|
65
|
+
|
|
66
|
+
class Config:
|
|
67
|
+
arbitrary_types_allowed = True # Allow numpy.ndarray type
|
arroyopy/timing.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
effective_level = logger.getEffectiveLevel()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EventTimingDecorator:
|
|
13
|
+
"""
|
|
14
|
+
Decorator to time functions and output the results in a pandas DataFrame.
|
|
15
|
+
|
|
16
|
+
This assumes within a single Event, there will be multiple calls to different functions.
|
|
17
|
+
When a new event starts, call `end_event` to store the timings and reset the timings for the next event.
|
|
18
|
+
When all events are done the timings can be accessed as a DataFrame using the `timing_dataframe` property.
|
|
19
|
+
After all events are done, call `reset` to clear all timings for the next event.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.current_event_times = {}
|
|
24
|
+
self.events = []
|
|
25
|
+
|
|
26
|
+
def __call__(self, func):
|
|
27
|
+
@functools.wraps(func)
|
|
28
|
+
def wrapper(*args, **kwargs):
|
|
29
|
+
start_time = time.time()
|
|
30
|
+
result = func(*args, **kwargs)
|
|
31
|
+
end_time = time.time()
|
|
32
|
+
duration = end_time - start_time
|
|
33
|
+
self.current_event_times[func.__name__] = duration
|
|
34
|
+
if effective_level == logging.DEBUG:
|
|
35
|
+
(f"{func.__name__} took {duration:.4f} seconds")
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
return wrapper
|
|
39
|
+
|
|
40
|
+
def end_event(self):
|
|
41
|
+
if self.current_event_times:
|
|
42
|
+
self.events.append(self.current_event_times)
|
|
43
|
+
self.current_event_times = {}
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def timing_dataframe(self):
|
|
47
|
+
return pd.DataFrame(self.current_event_times)
|
|
48
|
+
|
|
49
|
+
def reset(self):
|
|
50
|
+
self.current_event_times = {}
|
|
51
|
+
self.events = []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
timer = EventTimingDecorator()
|
arroyopy/zmq.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import zmq
|
|
5
|
+
import zmq.asyncio
|
|
6
|
+
|
|
7
|
+
from .listener import Listener
|
|
8
|
+
from .operator import Operator
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("arroyo.zmq")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ZMQListener(Listener):
|
|
14
|
+
stop_signal: bool = False
|
|
15
|
+
|
|
16
|
+
def __init__(self, operator: Operator, zmq_socket: zmq.Socket):
|
|
17
|
+
self.stop_requested = False
|
|
18
|
+
self.operator = operator
|
|
19
|
+
self.zmq_socket = zmq_socket
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_socket(cls, zmq_socket: zmq.Socket):
|
|
23
|
+
"""Construct a ZMQListenr using a provided socket. Gives
|
|
24
|
+
callers the ability to customize the ZMQ soket
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
zmq_socket : zmq.Socket
|
|
29
|
+
provided socket
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
ZMQListner
|
|
34
|
+
new ZMQListner
|
|
35
|
+
"""
|
|
36
|
+
return ZMQListener(zmq_socket)
|
|
37
|
+
|
|
38
|
+
async def start(self):
|
|
39
|
+
logger.info("Listener started")
|
|
40
|
+
# timeout after 100 milliseconds so we can be stopped if requested
|
|
41
|
+
self.zmq_socket.setsockopt(zmq.RCVTIMEO, 100)
|
|
42
|
+
while True:
|
|
43
|
+
if self.stop_requested:
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
msg = await self.zmq_socket.recv()
|
|
47
|
+
if logger.getEffectiveLevel() == logging.DEBUG:
|
|
48
|
+
logger.debug(f"{msg=}")
|
|
49
|
+
await self.operator.process(msg)
|
|
50
|
+
except zmq.Again:
|
|
51
|
+
# no message occured within the timeout period
|
|
52
|
+
pass
|
|
53
|
+
except asyncio.exceptions.CancelledError:
|
|
54
|
+
# in case this is being done in a asyncio.create_task call
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
async def stop(self):
|
|
58
|
+
self.stop_requested = True
|
|
59
|
+
self.zmq_socket.close()
|
|
60
|
+
self.zmq_socket.context.term()
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arroyopy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A library to simplify processing streams of data
|
|
5
|
+
Project-URL: Homepage, https://github.com/als-computing/arroyopy
|
|
6
|
+
Project-URL: Issues, https://github.com/als-computing/arroyopy/issues
|
|
7
|
+
Author-email: Dylan McReynolds <dmcreynolds@lbl.gov>
|
|
8
|
+
License: BSD-3
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: <3.13,>=3.11
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: pandas
|
|
15
|
+
Requires-Dist: pydantic>=2.0
|
|
16
|
+
Requires-Dist: python-dotenv
|
|
17
|
+
Requires-Dist: typer
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: fakeredis; extra == 'dev'
|
|
20
|
+
Requires-Dist: flake8; extra == 'dev'
|
|
21
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-mock; extra == 'dev'
|
|
24
|
+
Requires-Dist: pyzmq; extra == 'dev'
|
|
25
|
+
Requires-Dist: redis; extra == 'dev'
|
|
26
|
+
Requires-Dist: tiled[minimal-server]; extra == 'dev'
|
|
27
|
+
Provides-Extra: redis
|
|
28
|
+
Requires-Dist: redis; extra == 'redis'
|
|
29
|
+
Provides-Extra: tiled
|
|
30
|
+
Requires-Dist: tiled[client]; extra == 'tiled'
|
|
31
|
+
Provides-Extra: zmq
|
|
32
|
+
Requires-Dist: pyzmq; extra == 'zmq'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# Arroyo Stream Processing Toolset
|
|
36
|
+
|
|
37
|
+
Processing event or streaming data presents several technological challenges. A variety of technologies are often used by scientific user facilities. ZMQ is used to stream data and messages in a peer-to-peer fashion. Message brokers like Kafka, Redis and RabbitMQ are often employed to route and pass messages from instruments to processing workflows. Arroyo provides an API and structure to flexibly integrate with these tools and incorporate arbitrarily complex processing workflows, letting the hooks to the workflow code be independent of the connection code and hence reusable at a variety of instruments.
|
|
38
|
+
|
|
39
|
+
The basic structure of building an arroyo implementation is to implement groups of several classes:
|
|
40
|
+
-
|
|
41
|
+
- `Operator` - receives `Messages` from a listener and can optionally send `Messages` to one or more `Publisher` instances
|
|
42
|
+
- `Listener` - receives `Messages` from the external world, parse them into arroyo `Message` and sends them to an `Operator`
|
|
43
|
+
- `Publisher` - receives `Messages` from a `Listener` and publishes them to the outside world
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
Arroyo is un-opinionated about deployment decsions. It is intended support listener-operator-publisher groups in:
|
|
49
|
+
- Single process
|
|
50
|
+
- Chain of processes where listening, processing and publishing can linked together through a protocol like ZMQ. One process's publisher can communicate with another process's listener, etc.
|
|
51
|
+
|
|
52
|
+
This library is intended to provide classes, and will also include more specific common subclasses, like those that communicate over ZMQ or Redis.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
```mermaid
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
title: Some sweet classes
|
|
60
|
+
|
|
61
|
+
note: I guess we use "None" instead of "void"
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
classDiagram
|
|
65
|
+
namespace listener{
|
|
66
|
+
|
|
67
|
+
class Listener{
|
|
68
|
+
operator: Operator
|
|
69
|
+
|
|
70
|
+
*start(): None
|
|
71
|
+
*stop(): None
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
namespace operator{
|
|
78
|
+
class Operator{
|
|
79
|
+
publisher: List[Publisher]
|
|
80
|
+
*process(Message): None
|
|
81
|
+
add_publisher(Publisher): None
|
|
82
|
+
remove_publisher(Publisher): None
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
namespace publisher{
|
|
88
|
+
class Publisher{
|
|
89
|
+
*publish(Message): None
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
namespace message{
|
|
95
|
+
|
|
96
|
+
class Message{
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class Start{
|
|
101
|
+
data: Dict
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class Stop{
|
|
105
|
+
data: Dict
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class Event{
|
|
109
|
+
metadata: Dict
|
|
110
|
+
payload: bytes
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
namespace zmq{
|
|
115
|
+
class ZMQListener{
|
|
116
|
+
operator: Operator
|
|
117
|
+
socket: zmq.Socket
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class ZMQPublisher{
|
|
121
|
+
host: str
|
|
122
|
+
port: int
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
namespace redis{
|
|
128
|
+
|
|
129
|
+
class RedisListener{
|
|
130
|
+
operator: Redis.client
|
|
131
|
+
pubsub: Redis.pubsub
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
class RedisPublisher{
|
|
135
|
+
pubsub: Redis.pubsub
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
Listener <|-- ZMQListener
|
|
143
|
+
ZMQListener <|-- ZMQPubSubListener
|
|
144
|
+
Listener o-- Operator
|
|
145
|
+
|
|
146
|
+
Publisher <|-- ZMQPublisher
|
|
147
|
+
ZMQPublisher <|-- ZMQPubSubPublisher
|
|
148
|
+
|
|
149
|
+
Publisher <|-- RedisPublisher
|
|
150
|
+
Listener <|-- RedisListener
|
|
151
|
+
Operator o-- Publisher
|
|
152
|
+
Message <|-- Start
|
|
153
|
+
Message <|-- Stop
|
|
154
|
+
Message <|-- Event
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
##
|
|
159
|
+
In-process, listening for ZMQ
|
|
160
|
+
|
|
161
|
+
Note that this leaves Concrete classes undefined as placeholders
|
|
162
|
+
|
|
163
|
+
TODO: parent class labels
|
|
164
|
+
|
|
165
|
+
```mermaid
|
|
166
|
+
|
|
167
|
+
sequenceDiagram
|
|
168
|
+
autonumber
|
|
169
|
+
ExternalPublisher ->> ZMQPubSubListener: publish(bytes)
|
|
170
|
+
loop receiving thread
|
|
171
|
+
activate ZMQPubSubListener
|
|
172
|
+
ZMQPubSubListener ->> ConcreteMessageParser: parse(bytes)
|
|
173
|
+
ZMQPubSubListener ->> MessageQueue: put(bytes)
|
|
174
|
+
deactivate ZMQPubSubListener
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
ZMQPubSubListener ->> MessageQueue: message(Message)
|
|
178
|
+
end
|
|
179
|
+
activate ConcreteOperator
|
|
180
|
+
loop polling thread
|
|
181
|
+
ConcreteOperator ->> MessageQueue: get(bytes)
|
|
182
|
+
end
|
|
183
|
+
loop processing thread
|
|
184
|
+
ConcreteOperator ->> ConcreteOperator: calculate()
|
|
185
|
+
|
|
186
|
+
ConcreteOperator ->> ConcretePublisher: publish()
|
|
187
|
+
end
|
|
188
|
+
deactivate ConcreteOperator
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
# Devloper installation
|
|
192
|
+
|
|
193
|
+
## Conda environment
|
|
194
|
+
We use pixi to be forward thinking tio help with CI. We like it because it helps you easily test that dependencies for a variety of architects can resolve.
|
|
195
|
+
|
|
196
|
+
However, at the time of writing we can't figure out how to get it to be a good developer experience. So, we create a conda environment like (note that at this time, we are using python 3.11 because of numpy and wheel availability):
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
conda create -n arroyo python=3.11
|
|
200
|
+
conda activate arroyo
|
|
201
|
+
pip install -e '.[dev]'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## pre-commit
|
|
205
|
+
We use `pre-commit` in CI so you want to use it before commiting.
|
|
206
|
+
To test that your branches changes are all good, type:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
pre-commit run --all-files
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Since our configuration of `pre-commit` uses `black`, it's possible that it will change files. If you like the changes, you can add them to your `git` commit with
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
git add .
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Then you can run `pre-commit run --all-files` again.
|
|
219
|
+
|
|
220
|
+
## pixi
|
|
221
|
+
We use `pixi` for CI in github action. It's great for that but can't get our favorite developr tools to use the python environments that `pixi` creaetes in the `.pixi` folder. If you want to play with `pixi`, here are some tips:
|
|
222
|
+
|
|
223
|
+
To setup a development environment:
|
|
224
|
+
|
|
225
|
+
* Git clone this repo and CD into the directory
|
|
226
|
+
* Install [pixi](https://pixi.sh/v0.33.0/#installation)
|
|
227
|
+
* Install dependencies with
|
|
228
|
+
'''
|
|
229
|
+
pixi install
|
|
230
|
+
'''
|
|
231
|
+
* run pre-commit on the files
|
|
232
|
+
'''
|
|
233
|
+
pixi r pre-commit
|
|
234
|
+
'''
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
* Run pytest with
|
|
238
|
+
'''
|
|
239
|
+
pixi r test
|
|
240
|
+
'''
|
|
241
|
+
|
|
242
|
+
# Copyright
|
|
243
|
+
Arroyo Stream Processing Toolset (arroyopy) Copyright (c) 2025, The Regents of the University of California, through Lawrence Berkeley National Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy).
|
|
244
|
+
All rights reserved.
|
|
245
|
+
|
|
246
|
+
If you have questions about your rights to use or distribute this software,
|
|
247
|
+
please contact Berkeley Lab's Intellectual Property Office at
|
|
248
|
+
IPO@lbl.gov.
|
|
249
|
+
|
|
250
|
+
NOTICE. This Software was developed under funding from the U.S. Department
|
|
251
|
+
of Energy and the U.S. Government consequently retains certain rights. As
|
|
252
|
+
such, the U.S. Government has been granted for itself and others acting on
|
|
253
|
+
its behalf a paid-up, nonexclusive, irrevocable, worldwide license in the
|
|
254
|
+
Software to reproduce, distribute copies to the public, prepare derivative
|
|
255
|
+
works, and perform publicly and display publicly, and to permit others to do so.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
arroyopy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
arroyopy/listener.py,sha256=Cy_3UWkguFI__BUzk7d4DNbA52mK_ch8t1KaRFyr7dU,289
|
|
3
|
+
arroyopy/operator.py,sha256=-NvqKMbXt5O2ikJ9FhQIlnQilkQT0bbZ-1nRNmvsTx8,1574
|
|
4
|
+
arroyopy/publisher.py,sha256=a3bR5BI-B9pduxrTeR70jP1BIUyZNpu1CwYFvHvTCQA,259
|
|
5
|
+
arroyopy/redis.py,sha256=7OJSuKhDlHTDNtGvRmCApU0FKZ648KVs8bczySZz_sM,2098
|
|
6
|
+
arroyopy/schemas.py,sha256=ONARC_pormPXg7SNqlx1FPeU0A2knNMlspU3YEclxyk,1637
|
|
7
|
+
arroyopy/timing.py,sha256=HXhwQmpLq87JT5ZFZTh2vAQbO196whD6_Yw1yGjZdzI,1624
|
|
8
|
+
arroyopy/zmq.py,sha256=w8WPFYbwbLlmTPiIAc-pdNRpmUdbpkiyktBz1_7bXRA,1716
|
|
9
|
+
arroyopy-0.1.0.dist-info/METADATA,sha256=tgKvc43MUqIiElxXtbUoJd3UAu-myQhQXB38xvo_ZXY,7548
|
|
10
|
+
arroyopy-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
arroyopy-0.1.0.dist-info/licenses/LICENSE,sha256=jCk8QHKtWh2hrT0X2XXNZZDfreHb9v0Th5kZ3rDCN2g,2434
|
|
12
|
+
arroyopy-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Arroyo Stream Processing Toolset (arroyopy) Copyright (c) 2025, The Regents of the University of California, through Lawrence Berkeley National Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy).
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
(1) Redistributions of source code must retain the above copyright notice,
|
|
8
|
+
this list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
(2) Redistributions in binary form must reproduce the above copyright
|
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
|
12
|
+
documentation and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
(3) Neither the name of the University of California, Lawrence Berkeley
|
|
15
|
+
National Laboratory, U.S. Dept. of Energy nor the names of its contributors
|
|
16
|
+
may be used to endorse or promote products derived from this software
|
|
17
|
+
without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
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
|
|
21
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 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
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
You are under no obligation whatsoever to provide any bug fixes, patches,
|
|
25
|
+
or upgrades to the features, functionality or performance of the source
|
|
26
|
+
code ("Enhancements") to anyone; however, if you choose to make your
|
|
27
|
+
Enhancements available either publicly, or directly to Lawrence Berkeley
|
|
28
|
+
National Laboratory, without imposing a separate written license agreement
|
|
29
|
+
for such Enhancements, then you hereby grant the following license: a
|
|
30
|
+
non-exclusive, royalty-free perpetual license to install, use, modify,
|
|
31
|
+
prepare derivative works, incorporate into other computer software,
|
|
32
|
+
distribute, and sublicense such enhancements or derivative works thereof,
|
|
33
|
+
in binary and source code form.
|