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.
@@ -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}