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 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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)