task-lattice 0.0.1a0__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.
- task_lattice-0.0.1a0/LICENSE +21 -0
- task_lattice-0.0.1a0/PKG-INFO +132 -0
- task_lattice-0.0.1a0/README.md +114 -0
- task_lattice-0.0.1a0/pyproject.toml +35 -0
- task_lattice-0.0.1a0/src/task_lattice/__init__.py +0 -0
- task_lattice-0.0.1a0/src/task_lattice/app.py +95 -0
- task_lattice-0.0.1a0/src/task_lattice/broker.py +80 -0
- task_lattice-0.0.1a0/src/task_lattice/brokers/base.py +0 -0
- task_lattice-0.0.1a0/src/task_lattice/brokers/kafka.py +0 -0
- task_lattice-0.0.1a0/src/task_lattice/brokers/solace.py +83 -0
- task_lattice-0.0.1a0/src/task_lattice/config.py +25 -0
- task_lattice-0.0.1a0/src/task_lattice/py.typed +0 -0
- task_lattice-0.0.1a0/src/task_lattice/task.py +27 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nicholas Williams
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: task-lattice
|
|
3
|
+
Version: 0.0.1a0
|
|
4
|
+
Summary: Distributed Task Framework
|
|
5
|
+
Keywords: tasks,workflow,dag,distributed-tasks
|
|
6
|
+
Author: Nicholas Williams
|
|
7
|
+
Author-email: Nicholas Williams <noreply@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
<p>
|
|
20
|
+
<h3 style="font-size: 3.0em; margin: 0;">Task Lattice</h3>
|
|
21
|
+
<em>Distributed Task Framework for distributing work across workers</em>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="left">
|
|
25
|
+
<img src="https://github.com/nicholasfelixwilliams/task-lattice/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI">
|
|
26
|
+
<img src="https://img.shields.io/pypi/v/task-lattice?color=%2334D058&label=pypi%20package" alt="Package version">
|
|
27
|
+
<img src="https://img.shields.io/pypi/pyversions/task-lattice.svg?color=%2334D058" alt="Supported Python versions">
|
|
28
|
+
<img src="https://img.shields.io/static/v1?label=code%20style&message=ruff&color=black">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### đ Key Features
|
|
34
|
+
Task Lattice's key features include:
|
|
35
|
+
|
|
36
|
+
- **Broker Support** - Following brokers are supported:
|
|
37
|
+
- Solace
|
|
38
|
+
- Kafka (Day 2)
|
|
39
|
+
- **Queues** - Multiple queue/priority queue support
|
|
40
|
+
- **Async** - Async tasks supported incl. execution in event loop
|
|
41
|
+
- **Customisation** - Extensive customisation of queue, worker and tasks including:
|
|
42
|
+
- Automated task retry
|
|
43
|
+
- Dead letter queues
|
|
44
|
+
- Worker concurrency
|
|
45
|
+
- Queue capacity
|
|
46
|
+
- ...
|
|
47
|
+
- **Minimal code** - Minimal code is required to use task lattice
|
|
48
|
+
- **DAG support** - Supports DAG (directed acyclical graph) workflow execution
|
|
49
|
+
- **Monitoring** - Supports live monitoring of the queues, workers, tasks
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### âšī¸ Installation
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
# Using pip
|
|
57
|
+
pip install task-lattice
|
|
58
|
+
|
|
59
|
+
# Using poetry
|
|
60
|
+
poetry add task-lattice
|
|
61
|
+
|
|
62
|
+
# Using uv
|
|
63
|
+
uv add task-lattice
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### đĻ Dependencies
|
|
69
|
+
|
|
70
|
+
TBD
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### đ How to use
|
|
75
|
+
|
|
76
|
+
**Step 1 -** Define your Task Lattice application
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from task_lattice import TaskLattice, SolaceConnectionDetails, QueueDetails
|
|
80
|
+
|
|
81
|
+
app = TaskLattice(
|
|
82
|
+
SolaceConnectionDetails(host="localhost", port=55555, vpn="default", username="admin", password="admin"),
|
|
83
|
+
TaskLatticeConfig(
|
|
84
|
+
queues=[
|
|
85
|
+
QueueConfig(name="default", topic="tasks.default"),
|
|
86
|
+
],
|
|
87
|
+
default_queue="default"
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Step 2 -** Define your tasks:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
@app.task
|
|
96
|
+
def sync_function():
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
@app.task
|
|
100
|
+
async def async_function():
|
|
101
|
+
...
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Step 3 -** Enqueue a task:
|
|
105
|
+
```python
|
|
106
|
+
task = sync_function.create()
|
|
107
|
+
|
|
108
|
+
app.enqueue(task)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Step 4 -** Run a worker to process tasks:
|
|
112
|
+
```python
|
|
113
|
+
app.start_worker()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### đ Logging
|
|
119
|
+
|
|
120
|
+
TBD
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### đ Extensions
|
|
125
|
+
|
|
126
|
+
TBD
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### âšī¸ License
|
|
131
|
+
|
|
132
|
+
This project is licensed under the terms of the MIT license.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<h3 style="font-size: 3.0em; margin: 0;">Task Lattice</h3>
|
|
3
|
+
<em>Distributed Task Framework for distributing work across workers</em>
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="left">
|
|
7
|
+
<img src="https://github.com/nicholasfelixwilliams/task-lattice/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI">
|
|
8
|
+
<img src="https://img.shields.io/pypi/v/task-lattice?color=%2334D058&label=pypi%20package" alt="Package version">
|
|
9
|
+
<img src="https://img.shields.io/pypi/pyversions/task-lattice.svg?color=%2334D058" alt="Supported Python versions">
|
|
10
|
+
<img src="https://img.shields.io/static/v1?label=code%20style&message=ruff&color=black">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### đ Key Features
|
|
16
|
+
Task Lattice's key features include:
|
|
17
|
+
|
|
18
|
+
- **Broker Support** - Following brokers are supported:
|
|
19
|
+
- Solace
|
|
20
|
+
- Kafka (Day 2)
|
|
21
|
+
- **Queues** - Multiple queue/priority queue support
|
|
22
|
+
- **Async** - Async tasks supported incl. execution in event loop
|
|
23
|
+
- **Customisation** - Extensive customisation of queue, worker and tasks including:
|
|
24
|
+
- Automated task retry
|
|
25
|
+
- Dead letter queues
|
|
26
|
+
- Worker concurrency
|
|
27
|
+
- Queue capacity
|
|
28
|
+
- ...
|
|
29
|
+
- **Minimal code** - Minimal code is required to use task lattice
|
|
30
|
+
- **DAG support** - Supports DAG (directed acyclical graph) workflow execution
|
|
31
|
+
- **Monitoring** - Supports live monitoring of the queues, workers, tasks
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
### âšī¸ Installation
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
# Using pip
|
|
39
|
+
pip install task-lattice
|
|
40
|
+
|
|
41
|
+
# Using poetry
|
|
42
|
+
poetry add task-lattice
|
|
43
|
+
|
|
44
|
+
# Using uv
|
|
45
|
+
uv add task-lattice
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### đĻ Dependencies
|
|
51
|
+
|
|
52
|
+
TBD
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### đ How to use
|
|
57
|
+
|
|
58
|
+
**Step 1 -** Define your Task Lattice application
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from task_lattice import TaskLattice, SolaceConnectionDetails, QueueDetails
|
|
62
|
+
|
|
63
|
+
app = TaskLattice(
|
|
64
|
+
SolaceConnectionDetails(host="localhost", port=55555, vpn="default", username="admin", password="admin"),
|
|
65
|
+
TaskLatticeConfig(
|
|
66
|
+
queues=[
|
|
67
|
+
QueueConfig(name="default", topic="tasks.default"),
|
|
68
|
+
],
|
|
69
|
+
default_queue="default"
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Step 2 -** Define your tasks:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
@app.task
|
|
78
|
+
def sync_function():
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
@app.task
|
|
82
|
+
async def async_function():
|
|
83
|
+
...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Step 3 -** Enqueue a task:
|
|
87
|
+
```python
|
|
88
|
+
task = sync_function.create()
|
|
89
|
+
|
|
90
|
+
app.enqueue(task)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Step 4 -** Run a worker to process tasks:
|
|
94
|
+
```python
|
|
95
|
+
app.start_worker()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### đ Logging
|
|
101
|
+
|
|
102
|
+
TBD
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### đ Extensions
|
|
107
|
+
|
|
108
|
+
TBD
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### âšī¸ License
|
|
113
|
+
|
|
114
|
+
This project is licensed under the terms of the MIT license.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "task-lattice"
|
|
3
|
+
version = "0.0.1a"
|
|
4
|
+
description = "Distributed Task Framework"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Nicholas Williams", email = "noreply@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
keywords = ["tasks", "workflow", "dag", "distributed-tasks"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.12"
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.9.11,<0.10.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"mypy>=1.20.2",
|
|
30
|
+
"pytest>=9.0.3",
|
|
31
|
+
"pytest-asyncio>=1.3.0",
|
|
32
|
+
"pytest-cov>=7.1.0",
|
|
33
|
+
"ruff>=0.15.12",
|
|
34
|
+
"solace-pubsubplus>=1.11.0",
|
|
35
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from inspect import iscoroutinefunction
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from .broker import SolaceBroker
|
|
7
|
+
from .config import SolaceConnectionDetails, TaskLatticeConfig
|
|
8
|
+
from .task import Task, TaskInstance
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskLattice:
|
|
12
|
+
_task_registry: dict[str, Task]
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self, connection_details: SolaceConnectionDetails, config: TaskLatticeConfig
|
|
16
|
+
):
|
|
17
|
+
self.connection_details = connection_details
|
|
18
|
+
self.config = config
|
|
19
|
+
self.broker = SolaceBroker(self.connection_details)
|
|
20
|
+
|
|
21
|
+
self._task_registry = {}
|
|
22
|
+
|
|
23
|
+
def close(self):
|
|
24
|
+
self.broker.disconnect()
|
|
25
|
+
|
|
26
|
+
def task(self, f: Callable | None = None, *, name: str | None = None):
|
|
27
|
+
"""Decorator to register a python function as a TaskLattice task.
|
|
28
|
+
|
|
29
|
+
This must be applied to every task (sync or async) in the following way:
|
|
30
|
+
|
|
31
|
+
app = TaskLattice(...)
|
|
32
|
+
|
|
33
|
+
@app.task
|
|
34
|
+
def function(): ...
|
|
35
|
+
|
|
36
|
+
@app.task()
|
|
37
|
+
def function(): ...
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def decorator(func: Callable):
|
|
41
|
+
task_name = name or func.__name__
|
|
42
|
+
|
|
43
|
+
if task_name in self._task_registry:
|
|
44
|
+
raise ValueError(f"Task {task_name} is already registered")
|
|
45
|
+
|
|
46
|
+
task = Task(name=task_name, func=func, is_async=iscoroutinefunction(func))
|
|
47
|
+
|
|
48
|
+
self._task_registry[task.name] = task
|
|
49
|
+
|
|
50
|
+
# Attach TaskLattice methods
|
|
51
|
+
def create_task_instance(
|
|
52
|
+
args: list | None = None, kwargs: dict | None = None
|
|
53
|
+
):
|
|
54
|
+
return TaskInstance(task.name, self.config, args or [], kwargs or {})
|
|
55
|
+
|
|
56
|
+
func.create = create_task_instance
|
|
57
|
+
|
|
58
|
+
return func
|
|
59
|
+
|
|
60
|
+
if f is not None:
|
|
61
|
+
return decorator(f)
|
|
62
|
+
|
|
63
|
+
return decorator
|
|
64
|
+
|
|
65
|
+
def enqueue(self, task: TaskInstance):
|
|
66
|
+
self.broker.publish(task)
|
|
67
|
+
|
|
68
|
+
def start_worker(self):
|
|
69
|
+
loop = asyncio.get_event_loop()
|
|
70
|
+
asyncio.set_event_loop(loop)
|
|
71
|
+
|
|
72
|
+
executor = ThreadPoolExecutor()
|
|
73
|
+
|
|
74
|
+
def handle_message(message: dict):
|
|
75
|
+
task = self._task_registry.get(message["task_name"])
|
|
76
|
+
|
|
77
|
+
if task is None:
|
|
78
|
+
print(f"Unknown task: {message['task_name']}")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
if task.is_async:
|
|
82
|
+
asyncio.run_coroutine_threadsafe(
|
|
83
|
+
task.func(*message["args"], **message["kwargs"]), loop
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
loop.run_in_executor(
|
|
87
|
+
executor,
|
|
88
|
+
lambda: task.func(*message["args"], **message["kwargs"]),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# start broker consumer
|
|
92
|
+
self.broker.start_consumer(handle_message)
|
|
93
|
+
|
|
94
|
+
print("Worker started")
|
|
95
|
+
loop.run_forever()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from solace.messaging.messaging_service import MessagingService
|
|
3
|
+
from solace.messaging.receiver.message_receiver import MessageHandler
|
|
4
|
+
from solace.messaging.resources.topic import Topic
|
|
5
|
+
from solace.messaging.resources.queue import Queue
|
|
6
|
+
from solace.messaging.publisher.persistent_message_publisher import (
|
|
7
|
+
PersistentMessagePublisher,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from .config import SolaceConnectionDetails
|
|
11
|
+
from .task import TaskInstance
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SolaceBroker:
|
|
15
|
+
publisher: PersistentMessagePublisher
|
|
16
|
+
|
|
17
|
+
def __init__(self, connection_details: SolaceConnectionDetails):
|
|
18
|
+
self.connection_details = connection_details
|
|
19
|
+
|
|
20
|
+
config = {
|
|
21
|
+
"solace.messaging.transport.host": f"tcp://{self.connection_details.host}:{self.connection_details.port}",
|
|
22
|
+
"solace.messaging.service.vpn-name": self.connection_details.vpn,
|
|
23
|
+
"solace.messaging.authentication.scheme.basic.username": self.connection_details.username,
|
|
24
|
+
"solace.messaging.authentication.scheme.basic.password": self.connection_details.password,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
self.service = MessagingService.builder().from_properties(config).build()
|
|
28
|
+
self.publisher = (
|
|
29
|
+
self.service.create_persistent_message_publisher_builder().build()
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def connect(self):
|
|
33
|
+
self.service.connect()
|
|
34
|
+
self.publisher.start()
|
|
35
|
+
|
|
36
|
+
def ensure_connected(self):
|
|
37
|
+
if not self.service.is_connected:
|
|
38
|
+
self.service.connect()
|
|
39
|
+
if not self.publisher.is_ready():
|
|
40
|
+
self.publisher.start()
|
|
41
|
+
|
|
42
|
+
def disconnect(self):
|
|
43
|
+
self.publisher.terminate()
|
|
44
|
+
self.service.disconnect()
|
|
45
|
+
|
|
46
|
+
def publish(self, task: TaskInstance):
|
|
47
|
+
self.ensure_connected()
|
|
48
|
+
|
|
49
|
+
# Build the message
|
|
50
|
+
msg = (
|
|
51
|
+
self.service.message_builder()
|
|
52
|
+
.with_priority(task.priority)
|
|
53
|
+
.build(json.dumps(task.message))
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Publish the message
|
|
57
|
+
self.publisher.publish(msg, Topic.of("tasks.default"))
|
|
58
|
+
|
|
59
|
+
def start_consumer(self, handler):
|
|
60
|
+
self.ensure_connected()
|
|
61
|
+
receiver = self.service.create_persistent_message_receiver_builder().build(
|
|
62
|
+
Queue.durable_exclusive_queue("task_queue")
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
class MyHandler(MessageHandler):
|
|
66
|
+
def on_message(_, message):
|
|
67
|
+
payload = message.get_payload_as_string()
|
|
68
|
+
print("HI")
|
|
69
|
+
|
|
70
|
+
# deserialize
|
|
71
|
+
data = json.loads(payload)
|
|
72
|
+
|
|
73
|
+
print(data)
|
|
74
|
+
|
|
75
|
+
handler(data)
|
|
76
|
+
|
|
77
|
+
receiver.ack(message)
|
|
78
|
+
|
|
79
|
+
receiver.start()
|
|
80
|
+
receiver.receive_async(MyHandler())
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import json
|
|
4
|
+
from solace.messaging.messaging_service import MessagingService
|
|
5
|
+
from solace.messaging.receiver.message_receiver import MessageHandler
|
|
6
|
+
from solace.messaging.resources.topic import Topic
|
|
7
|
+
from solace.messaging.resources.queue import Queue
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class SolaceConnectionDetails:
|
|
12
|
+
host: str
|
|
13
|
+
port: int
|
|
14
|
+
vpn: str
|
|
15
|
+
username: str
|
|
16
|
+
password: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SolaceBroker:
|
|
20
|
+
def __init__(self, connection_details: SolaceConnectionDetails):
|
|
21
|
+
self.connection_details = connection_details
|
|
22
|
+
|
|
23
|
+
config = {
|
|
24
|
+
"solace.messaging.transport.host": f"tcp://{self.connection_details.host}:{self.connection_details.port}",
|
|
25
|
+
"solace.messaging.service.vpn-name": self.connection_details.vpn,
|
|
26
|
+
"solace.messaging.authentication.scheme.basic.username": self.connection_details.username,
|
|
27
|
+
"solace.messaging.authentication.scheme.basic.password": self.connection_details.password,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
self.service = MessagingService.builder().from_properties(config).build()
|
|
31
|
+
self.publisher = (
|
|
32
|
+
self.service.create_persistent_message_publisher_builder().build()
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def connect(self):
|
|
36
|
+
self.service.connect()
|
|
37
|
+
self.publisher.start()
|
|
38
|
+
|
|
39
|
+
def disconnect(self):
|
|
40
|
+
self.publisher.terminate()
|
|
41
|
+
self.service.disconnect()
|
|
42
|
+
|
|
43
|
+
def publish(self, message: dict, topic: str, priority: int):
|
|
44
|
+
sol_topic = Topic.of(topic)
|
|
45
|
+
|
|
46
|
+
msg_builder = self.service.message_builder()
|
|
47
|
+
msg_builder.with_priority(priority)
|
|
48
|
+
msg = msg_builder.build(json.dumps(message))
|
|
49
|
+
|
|
50
|
+
self.publisher.publish(msg, sol_topic)
|
|
51
|
+
|
|
52
|
+
# print(f"Published {msg} to {sol_topic}")
|
|
53
|
+
|
|
54
|
+
def listen_to_queue(self, queue: str):
|
|
55
|
+
q = Queue.durable_exclusive_queue(queue)
|
|
56
|
+
receiver = self.service.create_persistent_message_receiver_builder().build(q)
|
|
57
|
+
loop = asyncio.get_event_loop()
|
|
58
|
+
|
|
59
|
+
semaphore = asyncio.Semaphore(5)
|
|
60
|
+
|
|
61
|
+
async def process_message(payload: str):
|
|
62
|
+
print("âī¸ Processing async:", payload)
|
|
63
|
+
await asyncio.sleep(0.01)
|
|
64
|
+
|
|
65
|
+
# Proper handler class
|
|
66
|
+
class MyHandler(MessageHandler):
|
|
67
|
+
def on_message(self, message):
|
|
68
|
+
payload = message.get_payload_as_string()
|
|
69
|
+
print("đŠ Received:", payload)
|
|
70
|
+
|
|
71
|
+
async def wrapper():
|
|
72
|
+
async with semaphore:
|
|
73
|
+
await process_message(payload)
|
|
74
|
+
receiver.ack(message)
|
|
75
|
+
|
|
76
|
+
asyncio.run_coroutine_threadsafe(wrapper(), loop)
|
|
77
|
+
|
|
78
|
+
handler = MyHandler()
|
|
79
|
+
|
|
80
|
+
receiver.start()
|
|
81
|
+
receiver.receive_async(handler)
|
|
82
|
+
|
|
83
|
+
loop.run_forever()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class SolaceConnectionDetails:
|
|
6
|
+
host: str
|
|
7
|
+
port: int
|
|
8
|
+
vpn: str
|
|
9
|
+
|
|
10
|
+
# TODO: Allow multiple auth schemes
|
|
11
|
+
username: str
|
|
12
|
+
password: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class QueueConfig:
|
|
17
|
+
name: str
|
|
18
|
+
topic: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class TaskLatticeConfig:
|
|
23
|
+
queues: list[QueueConfig]
|
|
24
|
+
|
|
25
|
+
default_queue: str
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from .config import TaskLatticeConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Task:
|
|
9
|
+
name: str
|
|
10
|
+
func: Callable
|
|
11
|
+
is_async: bool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskInstance:
|
|
15
|
+
def __init__(
|
|
16
|
+
self, task_name: str, config: TaskLatticeConfig, args: list, kwargs: dict
|
|
17
|
+
) -> None:
|
|
18
|
+
self.task_name = task_name
|
|
19
|
+
self.args = args
|
|
20
|
+
self.kwargs = kwargs
|
|
21
|
+
|
|
22
|
+
self.priority = 2
|
|
23
|
+
self.topic = "tasks/default"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def message(self) -> dict:
|
|
27
|
+
return {"task_name": self.task_name, "args": self.args, "kwargs": self.kwargs}
|