twainmq 0.2.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.
- twainmq-0.2.3/LICENSE +21 -0
- twainmq-0.2.3/PKG-INFO +65 -0
- twainmq-0.2.3/README.md +39 -0
- twainmq-0.2.3/pyproject.toml +63 -0
- twainmq-0.2.3/setup.cfg +4 -0
- twainmq-0.2.3/src/twainmq/__init__.py +24 -0
- twainmq-0.2.3/src/twainmq/agent_base.py +44 -0
- twainmq-0.2.3/src/twainmq/atomic_append.py +100 -0
- twainmq-0.2.3/src/twainmq/consumer.py +357 -0
- twainmq-0.2.3/src/twainmq/consumer_groups.py +143 -0
- twainmq-0.2.3/src/twainmq/core.py +267 -0
- twainmq-0.2.3/src/twainmq/encoding.py +121 -0
- twainmq-0.2.3/src/twainmq/errors.py +14 -0
- twainmq-0.2.3/src/twainmq/producer.py +141 -0
- twainmq-0.2.3/src/twainmq/utils.py +41 -0
- twainmq-0.2.3/src/twainmq.egg-info/PKG-INFO +65 -0
- twainmq-0.2.3/src/twainmq.egg-info/SOURCES.txt +20 -0
- twainmq-0.2.3/src/twainmq.egg-info/dependency_links.txt +1 -0
- twainmq-0.2.3/src/twainmq.egg-info/requires.txt +9 -0
- twainmq-0.2.3/src/twainmq.egg-info/top_level.txt +1 -0
- twainmq-0.2.3/tests/test_encoding.py +34 -0
twainmq-0.2.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mike Crowe
|
|
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.
|
twainmq-0.2.3/PKG-INFO
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: twainmq
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: A file-based message queue. Kafka‑inspired. Twain‑simple.
|
|
5
|
+
Author-email: Mike Crowe <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/BlueRaven314/TwainMQ
|
|
8
|
+
Project-URL: repository, https://github.com/BlueRaven314/TwainMQ
|
|
9
|
+
Project-URL: issues, https://github.com/BlueRaven314/TwainMQ/issues
|
|
10
|
+
Keywords: messaging,message-queue,queue,pubsub,publish-subscribe,log-based,event-stream,event-log,file-based,lightweight,local,stream-processing,consumer-groups,partitioning,offsets,kafka-like
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: dataclasses-jsonschema>=2.16.0
|
|
18
|
+
Provides-Extra: docs
|
|
19
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
20
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
21
|
+
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
22
|
+
Requires-Dist: mkdocs-gen-files; extra == "docs"
|
|
23
|
+
Requires-Dist: mkdocs-literate-nav; extra == "docs"
|
|
24
|
+
Requires-Dist: mkdocs-section-index; extra == "docs"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="docs/TwainLogoTransparent.png" alt="TwainMQ Logo" width="200">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<h1 align="center">TwainMQ</h1>
|
|
32
|
+
|
|
33
|
+
**TwainMQ** is a lightweight, file‑based message log inspired by Kafka’s design principles but without the operational overhead of running a broker or cluster. It provides durable, append‑only topics, partitioned message streams, consumer groups with load balancing, and offset managemen, all without using a central service or broker.
|
|
34
|
+
|
|
35
|
+
TwainMQ is designed for:
|
|
36
|
+
|
|
37
|
+
- local development workflows
|
|
38
|
+
- embedded systems
|
|
39
|
+
- single‑machine (or small local network) data pipelines
|
|
40
|
+
- teaching and experimentation with log‑based messaging
|
|
41
|
+
- applications that need Kafka‑like semantics without a server
|
|
42
|
+
|
|
43
|
+
It requires **no services**, **no central authority**, just a directory where the files will live. All coordination is done via the log files.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **No running services or brokers**, just an API and distributed self-organisation
|
|
50
|
+
- **File‑based topics** stored as append‑only logs
|
|
51
|
+
- **Partitions** for parallelism and ordering guarantees
|
|
52
|
+
- **Consumer groups** with automatic load balancing
|
|
53
|
+
- **Offset tracking and commits**
|
|
54
|
+
- **Typed messages** (supports dataclasses)
|
|
55
|
+
- **Simple API** for producers and consumers
|
|
56
|
+
|
|
57
|
+
## Documentation
|
|
58
|
+
|
|
59
|
+
Full documentation is available at:
|
|
60
|
+
|
|
61
|
+
**https://blueraven314.github.io/TwainMQ/**
|
|
62
|
+
|
|
63
|
+
## Why “TwainMQ”?
|
|
64
|
+
|
|
65
|
+
Kafka messaging is powerful, but it’s also famously complex and sometimes hard going, rather like reading Franz Kafka. Mark Twain, on the other hand, is known for clarity and accessibility. TwainMQ takes inspiration from Kafka’s log‑based design, but aims to be simple, easy to use, and friendly to work with.
|
twainmq-0.2.3/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/TwainLogoTransparent.png" alt="TwainMQ Logo" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">TwainMQ</h1>
|
|
6
|
+
|
|
7
|
+
**TwainMQ** is a lightweight, file‑based message log inspired by Kafka’s design principles but without the operational overhead of running a broker or cluster. It provides durable, append‑only topics, partitioned message streams, consumer groups with load balancing, and offset managemen, all without using a central service or broker.
|
|
8
|
+
|
|
9
|
+
TwainMQ is designed for:
|
|
10
|
+
|
|
11
|
+
- local development workflows
|
|
12
|
+
- embedded systems
|
|
13
|
+
- single‑machine (or small local network) data pipelines
|
|
14
|
+
- teaching and experimentation with log‑based messaging
|
|
15
|
+
- applications that need Kafka‑like semantics without a server
|
|
16
|
+
|
|
17
|
+
It requires **no services**, **no central authority**, just a directory where the files will live. All coordination is done via the log files.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **No running services or brokers**, just an API and distributed self-organisation
|
|
24
|
+
- **File‑based topics** stored as append‑only logs
|
|
25
|
+
- **Partitions** for parallelism and ordering guarantees
|
|
26
|
+
- **Consumer groups** with automatic load balancing
|
|
27
|
+
- **Offset tracking and commits**
|
|
28
|
+
- **Typed messages** (supports dataclasses)
|
|
29
|
+
- **Simple API** for producers and consumers
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
Full documentation is available at:
|
|
34
|
+
|
|
35
|
+
**https://blueraven314.github.io/TwainMQ/**
|
|
36
|
+
|
|
37
|
+
## Why “TwainMQ”?
|
|
38
|
+
|
|
39
|
+
Kafka messaging is powerful, but it’s also famously complex and sometimes hard going, rather like reading Franz Kafka. Mark Twain, on the other hand, is known for clarity and accessibility. TwainMQ takes inspiration from Kafka’s log‑based design, but aims to be simple, easy to use, and friendly to work with.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "twainmq"
|
|
7
|
+
version = "0.2.3"
|
|
8
|
+
description = "A file-based message queue. Kafka‑inspired. Twain‑simple."
|
|
9
|
+
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Mike Crowe", email = "you@example.com" }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
license = { text = "MIT" }
|
|
16
|
+
requires-python = ">=3.8"
|
|
17
|
+
|
|
18
|
+
keywords = [
|
|
19
|
+
"messaging",
|
|
20
|
+
"message-queue",
|
|
21
|
+
"queue",
|
|
22
|
+
"pubsub",
|
|
23
|
+
"publish-subscribe",
|
|
24
|
+
"log-based",
|
|
25
|
+
"event-stream",
|
|
26
|
+
"event-log",
|
|
27
|
+
"file-based",
|
|
28
|
+
"lightweight",
|
|
29
|
+
"local",
|
|
30
|
+
"stream-processing",
|
|
31
|
+
"consumer-groups",
|
|
32
|
+
"partitioning",
|
|
33
|
+
"offsets",
|
|
34
|
+
"kafka-like"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
dependencies = [
|
|
38
|
+
"dataclasses-jsonschema>=2.16.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
classifiers = [
|
|
42
|
+
"Programming Language :: Python :: 3",
|
|
43
|
+
"License :: OSI Approved :: MIT License",
|
|
44
|
+
"Operating System :: OS Independent",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
homepage = "https://github.com/BlueRaven314/TwainMQ"
|
|
49
|
+
repository = "https://github.com/BlueRaven314/TwainMQ"
|
|
50
|
+
issues = "https://github.com/BlueRaven314/TwainMQ/issues"
|
|
51
|
+
|
|
52
|
+
[project.optional-dependencies]
|
|
53
|
+
docs = [
|
|
54
|
+
"mkdocs",
|
|
55
|
+
"mkdocs-material",
|
|
56
|
+
"mkdocstrings[python]",
|
|
57
|
+
"mkdocs-gen-files",
|
|
58
|
+
"mkdocs-literate-nav",
|
|
59
|
+
"mkdocs-section-index",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.setuptools.packages.find]
|
|
63
|
+
where = ["src"]
|
twainmq-0.2.3/setup.cfg
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
TwainMQ is a broker free messaging system. Unlike similar tools like Apache Kafka and RabbitMQ, TwainMQ does not have any running services or brokers to coordinate messages, being run entirely from files. Think of it like SQLite is to traditional database systems.
|
|
4
|
+
|
|
5
|
+
To set up a TwainMQ message queue you simply need to specify a directory for it to use, either locally or on a network share.
|
|
6
|
+
|
|
7
|
+
Messages are limited in size to ensure that they can be written quickly. The size limit is currently set at slightly less than 4kb.
|
|
8
|
+
|
|
9
|
+
Messages can be either plain UTF-8 or raw bytes.
|
|
10
|
+
|
|
11
|
+
Messages are organised by key, partition and topic.
|
|
12
|
+
|
|
13
|
+
## Keys
|
|
14
|
+
|
|
15
|
+
u8
|
|
16
|
+
u16
|
|
17
|
+
u32
|
|
18
|
+
u64
|
|
19
|
+
str
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .core import Twain, TwainMQConsumer, TwainMQProducer
|
|
24
|
+
from .utils import multi_poll
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from twainmq.encoding import find_key_char_width
|
|
6
|
+
from twainmq.errors import ConfigNotFoundError
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .core import Twain
|
|
12
|
+
|
|
13
|
+
class TwainMQBase(ABC):
|
|
14
|
+
def __init__(self, twain: "Twain", topic: str):
|
|
15
|
+
self._topic = topic
|
|
16
|
+
self._twain = twain
|
|
17
|
+
config_path = self._twain._config_path(topic)
|
|
18
|
+
if not config_path.is_file():
|
|
19
|
+
raise ConfigNotFoundError(f"No config for {topic}: cannot find file {config_path}")
|
|
20
|
+
with config_path.open("r") as f:
|
|
21
|
+
config = json.load(f)
|
|
22
|
+
self._key_width = config["key_width"]
|
|
23
|
+
self._message_types = config["message_types"]
|
|
24
|
+
self._message_types_rev = {i: msg_type for msg_type, i in self._message_types.items()}
|
|
25
|
+
self._n_partitions = int(config["partitions"])
|
|
26
|
+
self._key_chars = find_key_char_width(self._key_width)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def topic(self):
|
|
30
|
+
return self._topic
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def _topic_dir(self):
|
|
34
|
+
return self._twain.root_dir / self._topic
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def key_width(self):
|
|
38
|
+
"""The width of the key in bytes. 0 indicates a string key.
|
|
39
|
+
"""
|
|
40
|
+
return self._key_width
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def chunk_str_now(self):
|
|
44
|
+
return f"{datetime.now(timezone.utc):%Y%m%d}"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import errno
|
|
4
|
+
import ctypes
|
|
5
|
+
from ctypes import wintypes
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import os
|
|
8
|
+
import errno
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Win32 constants
|
|
12
|
+
FILE_APPEND_DATA = 0x0004
|
|
13
|
+
FILE_SHARE_READ = 0x00000001
|
|
14
|
+
FILE_SHARE_WRITE = 0x00000002
|
|
15
|
+
OPEN_ALWAYS = 4
|
|
16
|
+
FILE_ATTRIBUTE_NORMAL = 0x80
|
|
17
|
+
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
|
|
18
|
+
|
|
19
|
+
if os.name == "nt":
|
|
20
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
21
|
+
|
|
22
|
+
def atomic_append_win(path, data: bytes) -> None:
|
|
23
|
+
"""Atomic appending to a file under Windows.
|
|
24
|
+
|
|
25
|
+
This makes use of the windows FILE_APPEND_DATA mode which doesn't check the file length first or use a cursor
|
|
26
|
+
it simply dumps the data on the end of the file.
|
|
27
|
+
"""
|
|
28
|
+
# Accept Path or str
|
|
29
|
+
if isinstance(path, Path):
|
|
30
|
+
path = str(path)
|
|
31
|
+
|
|
32
|
+
# 1. Open with FILE_APPEND_DATA only (critical)
|
|
33
|
+
handle = kernel32.CreateFileW(
|
|
34
|
+
wintypes.LPCWSTR(path),
|
|
35
|
+
FILE_APPEND_DATA, # <-- atomic append mode
|
|
36
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
37
|
+
None,
|
|
38
|
+
OPEN_ALWAYS,
|
|
39
|
+
FILE_ATTRIBUTE_NORMAL,
|
|
40
|
+
None,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if handle == INVALID_HANDLE_VALUE:
|
|
44
|
+
raise OSError(ctypes.get_last_error(), "CreateFileW failed")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# 2. WriteFile (one syscall, atomic append)
|
|
48
|
+
written = wintypes.DWORD(0)
|
|
49
|
+
ok = kernel32.WriteFile(
|
|
50
|
+
handle,
|
|
51
|
+
data,
|
|
52
|
+
len(data),
|
|
53
|
+
ctypes.byref(written),
|
|
54
|
+
None,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if not ok or written.value != len(data):
|
|
58
|
+
raise OSError(ctypes.get_last_error(), "WriteFile failed")
|
|
59
|
+
|
|
60
|
+
finally:
|
|
61
|
+
kernel32.CloseHandle(handle)
|
|
62
|
+
|
|
63
|
+
def atomic_append_posix(path, data: bytes, mode: int = 0o644) -> None:
|
|
64
|
+
"""Atomic append function for POSIX systems.
|
|
65
|
+
|
|
66
|
+
This should work by using O_APPEND but this is not properly tested yet!
|
|
67
|
+
|
|
68
|
+
Short write might in theory happen, but shouldn't, when we can test this better
|
|
69
|
+
we will know if this is a problem and come up with a solution.
|
|
70
|
+
"""
|
|
71
|
+
# Accept Path or str
|
|
72
|
+
if isinstance(path, Path):
|
|
73
|
+
path = os.fspath(path)
|
|
74
|
+
|
|
75
|
+
# Open with O_APPEND so the kernel moves the file offset atomically
|
|
76
|
+
flags = os.O_WRONLY | os.O_APPEND | os.O_CREAT
|
|
77
|
+
|
|
78
|
+
# Add close-on-exec if available (good hygiene)
|
|
79
|
+
flags |= getattr(os, "O_CLOEXEC", 0)
|
|
80
|
+
|
|
81
|
+
fd = os.open(path, flags, mode)
|
|
82
|
+
try:
|
|
83
|
+
# Exactly one write() syscall
|
|
84
|
+
n = os.write(fd, data)
|
|
85
|
+
|
|
86
|
+
# POSIX guarantees full writes to regular files unless an error occurs.
|
|
87
|
+
# If we ever see a short write, treat it as fatal.
|
|
88
|
+
if n != len(data):
|
|
89
|
+
raise OSError(errno.EIO, f"short write: wrote {n} of {len(data)} bytes")
|
|
90
|
+
|
|
91
|
+
finally:
|
|
92
|
+
os.close(fd)
|
|
93
|
+
|
|
94
|
+
def atomic_append(path, data: bytes, mode: int = 0o644) -> None:
|
|
95
|
+
"""Atomic writing used by producers to allow multiple producers to write to the same
|
|
96
|
+
log file at the same time without locks"""
|
|
97
|
+
if os.name == "nt":
|
|
98
|
+
atomic_append_win(path, data)
|
|
99
|
+
else:
|
|
100
|
+
atomic_append_posix(path, data)
|