ropemother 0.1.0.dev8__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.
Files changed (97) hide show
  1. ropemother/__init__.py +46 -0
  2. ropemother/bootstrap/__init__.py +11 -0
  3. ropemother/bootstrap/buffer.py +127 -0
  4. ropemother/bootstrap/policy.py +105 -0
  5. ropemother/broker/__init__.py +37 -0
  6. ropemother/broker/asyncbase.py +58 -0
  7. ropemother/broker/asyncdirect.py +349 -0
  8. ropemother/broker/asyncendpoints.py +106 -0
  9. ropemother/broker/base.py +59 -0
  10. ropemother/broker/direct.py +338 -0
  11. ropemother/broker/directcore.py +863 -0
  12. ropemother/broker/dispatch.py +66 -0
  13. ropemother/broker/endpoints.py +152 -0
  14. ropemother/broker/subscription.py +34 -0
  15. ropemother/capture/__init__.py +52 -0
  16. ropemother/capture/controller.py +207 -0
  17. ropemother/capture/filehistory.py +239 -0
  18. ropemother/capture/filesink.py +72 -0
  19. ropemother/capture/history.py +405 -0
  20. ropemother/capture/historyselection.py +69 -0
  21. ropemother/capture/historyservice.py +763 -0
  22. ropemother/capture/jsonrecords.py +251 -0
  23. ropemother/capture/memorysink.py +64 -0
  24. ropemother/capture/runtime.py +33 -0
  25. ropemother/capture/sink.py +64 -0
  26. ropemother/capture/writer.py +83 -0
  27. ropemother/client/__init__.py +58 -0
  28. ropemother/client/asyncendpointfactory.py +146 -0
  29. ropemother/client/asyncendpointprovisioner.py +383 -0
  30. ropemother/client/asyncrequest.py +405 -0
  31. ropemother/client/endpointfactory.py +141 -0
  32. ropemother/client/endpointfactorybase.py +378 -0
  33. ropemother/client/lifecycle.py +75 -0
  34. ropemother/client/procedure.py +151 -0
  35. ropemother/client/request.py +568 -0
  36. ropemother/client/requestoptions.py +23 -0
  37. ropemother/exceptions.py +61 -0
  38. ropemother/fixtures/__init__.py +30 -0
  39. ropemother/fixtures/scriptedinput.py +437 -0
  40. ropemother/format/__init__.py +60 -0
  41. ropemother/format/defaults.py +42 -0
  42. ropemother/format/formattable.py +39 -0
  43. ropemother/format/portableformat.py +191 -0
  44. ropemother/format/registry.py +201 -0
  45. ropemother/message/__init__.py +37 -0
  46. ropemother/message/messageidentity.py +23 -0
  47. ropemother/message/records.py +121 -0
  48. ropemother/message/registrationtable.py +185 -0
  49. ropemother/message/selectors.py +191 -0
  50. ropemother/message/symbols.py +241 -0
  51. ropemother/message/typeformats.py +239 -0
  52. ropemother/playground.py +7841 -0
  53. ropemother/service/__init__.py +61 -0
  54. ropemother/service/__main__.py +19 -0
  55. ropemother/service/asyncservice.py +184 -0
  56. ropemother/service/broker.py +278 -0
  57. ropemother/service/brokerextension.py +31 -0
  58. ropemother/service/brokerhistory.py +78 -0
  59. ropemother/service/connector.py +91 -0
  60. ropemother/service/descriptor.py +104 -0
  61. ropemother/service/environment.py +161 -0
  62. ropemother/service/host.py +272 -0
  63. ropemother/service/listener.py +47 -0
  64. ropemother/service/resource.py +58 -0
  65. ropemother/service/service.py +200 -0
  66. ropemother/service/socketlistener.py +280 -0
  67. ropemother/transport/__init__.py +46 -0
  68. ropemother/transport/asyncclient.py +694 -0
  69. ropemother/transport/asyncconnection.py +101 -0
  70. ropemother/transport/asyncsession.py +304 -0
  71. ropemother/transport/asyncsessionrunner.py +108 -0
  72. ropemother/transport/asyncsocketconnection.py +102 -0
  73. ropemother/transport/client.py +679 -0
  74. ropemother/transport/codec.py +743 -0
  75. ropemother/transport/connection.py +105 -0
  76. ropemother/transport/endpointregistration.py +86 -0
  77. ropemother/transport/frames.py +150 -0
  78. ropemother/transport/session.py +293 -0
  79. ropemother/transport/sessionrunner.py +107 -0
  80. ropemother/transport/sessionstate.py +119 -0
  81. ropemother/transport/socketconnection.py +81 -0
  82. ropemother/transport/socketframing.py +98 -0
  83. ropemother/transport/zeromq/__init__.py +11 -0
  84. ropemother/transport/zeromq/address.py +29 -0
  85. ropemother/transport/zeromq/connection.py +169 -0
  86. ropemother/util/__init__.py +57 -0
  87. ropemother/util/compositeblobserializer.py +110 -0
  88. ropemother/util/lengthprefixed.py +112 -0
  89. ropemother/util/onelinejson.py +62 -0
  90. ropemother/util/serializer.py +101 -0
  91. ropemother/util/symbol.py +34 -0
  92. ropemother/util/typedid.py +66 -0
  93. ropemother-0.1.0.dev8.dist-info/METADATA +462 -0
  94. ropemother-0.1.0.dev8.dist-info/RECORD +97 -0
  95. ropemother-0.1.0.dev8.dist-info/WHEEL +5 -0
  96. ropemother-0.1.0.dev8.dist-info/licenses/LICENSE +9 -0
  97. ropemother-0.1.0.dev8.dist-info/top_level.txt +1 -0
ropemother/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ # ropemother/__init__.py
3
+
4
+ """A module for publish-subscribe broadcast messaging."""
5
+
6
+ from importlib import import_module
7
+ from typing import Any
8
+
9
+ __author__ = "Joe Granville"
10
+ __email__ = "874605+jwgranville@users.noreply.github.com"
11
+ __date__ = "2026-07-05T15:40:19+00:00"
12
+ __license__ = "MIT"
13
+ __version__ = "0.1.0.dev8"
14
+ __status__ = "Development"
15
+
16
+
17
+ _EXPORTS = {
18
+ "AsyncDirectMessageBus": "ropemother.broker.asyncdirect",
19
+ "CAPTURE_PORTABLE_FORMAT": "ropemother.format.portableformat",
20
+ "CaptureMode": "ropemother.broker.directcore",
21
+ "DirectMessageBus": "ropemother.broker.direct",
22
+ "InMemoryCaptureSink": "ropemother.capture.memorysink",
23
+ "JSONLinesCaptureSink": "ropemother.capture.filesink",
24
+ "JSON_PORTABLE_FORMAT": "ropemother.format.portableformat",
25
+ "MessageBusBaseException": "ropemother.exceptions",
26
+ "PayloadSerializationError": "ropemother.exceptions",
27
+ "PortableFormat": "ropemother.format.portableformat",
28
+ "RAW_BYTES_PORTABLE_FORMAT": "ropemother.format.portableformat",
29
+ "ReceivedMessage": "ropemother.message.records",
30
+ "connect_async_message_bus": "ropemother.service.environment",
31
+ "connect_message_bus": "ropemother.service.environment",
32
+ "exact_topic": "ropemother.message.selectors",
33
+ "topic_tree": "ropemother.message.selectors",
34
+ }
35
+
36
+ __all__ = list(_EXPORTS)
37
+
38
+
39
+ def __getattr__(name: str) -> Any:
40
+ if name not in _EXPORTS:
41
+ raise AttributeError(name)
42
+
43
+ module = import_module(_EXPORTS[name])
44
+ value = getattr(module, name)
45
+ globals()[name] = value
46
+ return value
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env python3
2
+ # ropemother/bootstrap/__init__.py
3
+
4
+ """Implements startup message buffering policy."""
5
+
6
+ __author__ = "Joe Granville"
7
+ __email__ = "874605+jwgranville@users.noreply.github.com"
8
+ __date__ = "2026-07-01T20:13:04+00:00"
9
+ __license__ = "MIT"
10
+ __version__ = "0.1.0.dev8"
11
+ __status__ = "Development"
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # ropemother/bootstrap/buffer.py
3
+
4
+ """Support for bootstrap buffering of capture records at startup."""
5
+
6
+ from dataclasses import dataclass
7
+
8
+ from ropemother.capture.sink import CaptureSink
9
+ from ropemother.capture.writer import CaptureRecord, CaptureRecordWriter
10
+ from ropemother.exceptions import MessageBusBaseException
11
+ from ropemother.format.registry import PortableFormatRegistration
12
+ from ropemother.message.records import CapturedMessage
13
+ from ropemother.message.symbols import MessageSymbolRegistration
14
+
15
+ __author__ = "Joe Granville"
16
+ __email__ = "874605+jwgranville@users.noreply.github.com"
17
+ __date__ = "2026-07-02T07:00:19+00:00"
18
+ __license__ = "MIT"
19
+ __version__ = "0.1.0.dev8"
20
+ __status__ = "Development"
21
+
22
+
23
+ DEFAULT_BOOTSTRAP_BUFFER_BYTES = 4 * 1024 * 1024
24
+ DEFAULT_BOOTSTRAP_BUFFER_RECORD_LIMIT = 256
25
+
26
+
27
+ class BootstrapBufferError(MessageBusBaseException):
28
+ """Base exception for bootstrap buffer errors."""
29
+ pass
30
+
31
+
32
+ class InvalidBootstrapBufferLimitError(ValueError, BootstrapBufferError):
33
+ """Raised when bootstrap buffer limits are invalid."""
34
+ pass
35
+
36
+
37
+ class BootstrapBufferLimitExceededError(BufferError, BootstrapBufferError):
38
+ """Raised when bootstrap buffering exceeds configured limits."""
39
+ pass
40
+
41
+
42
+ @dataclass(frozen=True, kw_only=True)
43
+ class BootstrapBufferLimits:
44
+ """Record and payload byte limits for bootstrap buffering."""
45
+ max_records: int = DEFAULT_BOOTSTRAP_BUFFER_RECORD_LIMIT
46
+ max_payload_bytes: int = DEFAULT_BOOTSTRAP_BUFFER_BYTES
47
+
48
+ def __post_init__(self) -> None:
49
+ if self.max_records < 1:
50
+ raise InvalidBootstrapBufferLimitError(
51
+ "bootstrap buffer record limit must be positive"
52
+ )
53
+ if self.max_payload_bytes < 0:
54
+ raise InvalidBootstrapBufferLimitError(
55
+ "bootstrap buffer payload byte limit must not be negative"
56
+ )
57
+
58
+
59
+ class BootstrapBuffer(CaptureRecordWriter):
60
+ """Temporary capture record writer used during broker bootstrap."""
61
+ _limits: BootstrapBufferLimits
62
+ _records: list[CaptureRecord]
63
+ _payload_bytes: int
64
+
65
+ def __init__(self, *, limits: BootstrapBufferLimits | None = None) -> None:
66
+ if limits is None:
67
+ limits = BootstrapBufferLimits()
68
+
69
+ self._limits = limits
70
+ self._records = []
71
+ self._payload_bytes = 0
72
+
73
+ @property
74
+ def pending_record_count(self) -> int:
75
+ return len(self._records)
76
+
77
+ @property
78
+ def pending_payload_bytes(self) -> int:
79
+ return self._payload_bytes
80
+
81
+ def pending_records(self) -> tuple[CaptureRecord, ...]:
82
+ return tuple(self._records)
83
+
84
+ def clear(self) -> None:
85
+ self._records.clear()
86
+ self._payload_bytes = 0
87
+
88
+ def flush_to(self, capture_sink: CaptureSink) -> None:
89
+ for record in self._records:
90
+ capture_sink.write_record(record)
91
+
92
+ self.clear()
93
+
94
+ def write_message_record(self, message: CapturedMessage) -> None:
95
+ self._append_record(message)
96
+
97
+ def write_format_registration(
98
+ self, registration: PortableFormatRegistration
99
+ ) -> None:
100
+ self._append_record(registration)
101
+
102
+ def write_symbol_registration(
103
+ self, registration: MessageSymbolRegistration
104
+ ) -> None:
105
+ self._append_record(registration)
106
+
107
+ def _append_record(self, record: CaptureRecord) -> None:
108
+ candidate_payload_bytes = self._payload_byte_count(record)
109
+ next_record_count = len(self._records) + 1
110
+ next_payload_bytes = self._payload_bytes + candidate_payload_bytes
111
+
112
+ if next_record_count > self._limits.max_records:
113
+ raise BootstrapBufferLimitExceededError(
114
+ "bootstrap buffer record limit exceeded"
115
+ )
116
+ if next_payload_bytes > self._limits.max_payload_bytes:
117
+ raise BootstrapBufferLimitExceededError(
118
+ "bootstrap buffer payload byte limit exceeded"
119
+ )
120
+
121
+ self._records.append(record)
122
+ self._payload_bytes = next_payload_bytes
123
+
124
+ def _payload_byte_count(self, record: CaptureRecord) -> int:
125
+ if isinstance(record, CapturedMessage):
126
+ return len(record.serialized_payload.payload_bytes)
127
+ return 0
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ # ropemother/bootstrap/policy.py
3
+
4
+ """Bootstrap traffic policy objects."""
5
+
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass
8
+ from enum import StrEnum
9
+
10
+ from ropemother.exceptions import MessageBusBaseException
11
+ from ropemother.message.records import BusMessage, BusOperation
12
+
13
+ __author__ = "Joe Granville"
14
+ __email__ = "874605+jwgranville@users.noreply.github.com"
15
+ __date__ = "2026-07-02T07:03:33+00:00"
16
+ __license__ = "MIT"
17
+ __version__ = "0.1.0.dev8"
18
+ __status__ = "Development"
19
+
20
+
21
+ DEFAULT_LIFECYCLE_TOPIC_ROOT = "lifecycle"
22
+ DEFAULT_LIFECYCLE_OPERATIONS = frozenset({BusOperation.PUBLISH})
23
+
24
+
25
+ class BootstrapPolicyError(MessageBusBaseException):
26
+ """Base exception for bootstrap policy errors."""
27
+ pass
28
+
29
+
30
+ class BootstrapMessageRejectedError(RuntimeError, BootstrapPolicyError):
31
+ """Raised when bootstrap message traffic is rejected."""
32
+ pass
33
+
34
+
35
+ class LifecycleMessageType(StrEnum):
36
+ """Built-in lifecycle message types allowed during bootstrap."""
37
+ STARTED = "started"
38
+ READY = "ready"
39
+ FAILED = "failed"
40
+ STOPPING = "stopping"
41
+ STOPPED = "stopped"
42
+
43
+
44
+ DEFAULT_LIFECYCLE_MESSAGE_TYPES = frozenset(
45
+ message_type.value for message_type in LifecycleMessageType
46
+ )
47
+
48
+
49
+ @dataclass(frozen=True, kw_only=True)
50
+ class BootstrapDecision:
51
+ """Decision result for bootstrap message traffic."""
52
+ accepted: bool
53
+ reason: str = ""
54
+
55
+
56
+ class BootstrapPolicy(ABC):
57
+ """Policy interface for deciding bootstrap message traffic."""
58
+ @abstractmethod
59
+ def decide_message(self, message: BusMessage) -> BootstrapDecision:
60
+ pass
61
+
62
+
63
+ class RejectBootstrapPolicy(BootstrapPolicy):
64
+ """Bootstrap policy that rejects all message traffic."""
65
+ def decide_message(self, message: BusMessage) -> BootstrapDecision:
66
+ decision = BootstrapDecision(
67
+ accepted=False,
68
+ reason="bootstrap message traffic is not allowed",
69
+ )
70
+ return decision
71
+
72
+
73
+ @dataclass(frozen=True, kw_only=True)
74
+ class LifecycleBootstrapPolicy(BootstrapPolicy):
75
+ """Bootstrap policy that permits selected lifecycle messages."""
76
+ topic_root: str = DEFAULT_LIFECYCLE_TOPIC_ROOT
77
+ msg_types: frozenset[str] = DEFAULT_LIFECYCLE_MESSAGE_TYPES
78
+ operations: frozenset[BusOperation] = DEFAULT_LIFECYCLE_OPERATIONS
79
+
80
+ def decide_message(self, message: BusMessage) -> BootstrapDecision:
81
+ if message.bus_operation not in self.operations:
82
+ decision = BootstrapDecision(
83
+ accepted=False,
84
+ reason="bootstrap message operation is not allowed",
85
+ )
86
+ elif not _topic_matches_root(message.msg_topic, self.topic_root):
87
+ decision = BootstrapDecision(
88
+ accepted=False,
89
+ reason="bootstrap message topic is not allowed",
90
+ )
91
+ elif message.msg_type not in self.msg_types:
92
+ decision = BootstrapDecision(
93
+ accepted=False,
94
+ reason="bootstrap message type is not allowed",
95
+ )
96
+ else:
97
+ decision = BootstrapDecision(accepted=True)
98
+
99
+ return decision
100
+
101
+
102
+ def _topic_matches_root(msg_topic: str, topic_root: str) -> bool:
103
+ matches_root = msg_topic == topic_root
104
+ matches_child = msg_topic.startswith(topic_root + ".")
105
+ return matches_root or matches_child
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ # ropemother/broker/__init__.py
3
+
4
+ """Supports and implements runtime publish-subscribe services."""
5
+
6
+ from importlib import import_module
7
+ from typing import Any
8
+
9
+ __author__ = "Joe Granville"
10
+ __email__ = "874605+jwgranville@users.noreply.github.com"
11
+ __date__ = "2026-06-30T18:59:10+00:00"
12
+ __license__ = "MIT"
13
+ __version__ = "0.1.0.dev8"
14
+ __status__ = "Development"
15
+
16
+
17
+ _EXPORTS = {
18
+ "AsyncDirectMessageBus": "ropemother.broker.asyncdirect",
19
+ "AsyncEmitter": "ropemother.broker.asyncendpoints",
20
+ "AsyncReceiver": "ropemother.broker.asyncendpoints",
21
+ "CaptureMode": "ropemother.broker.directcore",
22
+ "DirectMessageBus": "ropemother.broker.direct",
23
+ "Emitter": "ropemother.broker.endpoints",
24
+ "Receiver": "ropemother.broker.endpoints",
25
+ }
26
+
27
+ __all__ = list(_EXPORTS)
28
+
29
+
30
+ def __getattr__(name: str) -> Any:
31
+ if name not in _EXPORTS:
32
+ raise AttributeError(name)
33
+
34
+ module = import_module(_EXPORTS[name])
35
+ value = getattr(module, name)
36
+ globals()[name] = value
37
+ return value
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+ # ropemother/broker/asyncbase.py
3
+
4
+ """Asynchronous abstract interface for message bus implementation."""
5
+
6
+ import abc
7
+
8
+ from ropemother.broker.asyncendpoints import AsyncEmitter, AsyncReceiver
9
+ from ropemother.capture.sink import CaptureSink
10
+ from ropemother.client.asyncendpointfactory import AsyncMessageEndpointFactory
11
+ from ropemother.format.portableformat import (
12
+ PortableFormat,
13
+ JSON_PORTABLE_FORMAT,
14
+ )
15
+ from ropemother.message.selectors import (
16
+ OptionalSymbolInput,
17
+ SubscriptionTopicInput,
18
+ SymbolCollectionInput,
19
+ )
20
+ from ropemother.message.typeformats import SupportedTypeFormatsInput
21
+
22
+ __author__ = "Joe Granville"
23
+ __email__ = "874605+jwgranville@users.noreply.github.com"
24
+ __date__ = "2026-08-20T17:37:34+00:00"
25
+ __license__ = "MIT"
26
+ __version__ = "0.1.0.dev8"
27
+ __status__ = "Development"
28
+
29
+
30
+ class AsyncMessageBus(AsyncMessageEndpointFactory):
31
+ """Abstract broker interface for async message bus implementations."""
32
+ @abc.abstractmethod
33
+ def register_emitter(
34
+ self,
35
+ *,
36
+ msg_topic: str,
37
+ msg_producer: str,
38
+ msg_type: str,
39
+ additional_msg_types: SymbolCollectionInput = (),
40
+ allow_unlisted_type_formats: bool = False,
41
+ payload_format: PortableFormat = JSON_PORTABLE_FORMAT,
42
+ supported_type_formats: SupportedTypeFormatsInput | None = None,
43
+ ) -> AsyncEmitter:
44
+ ...
45
+
46
+ @abc.abstractmethod
47
+ def subscribe(
48
+ self,
49
+ *,
50
+ msg_topic: SubscriptionTopicInput,
51
+ msg_producer: OptionalSymbolInput = None,
52
+ msg_type: OptionalSymbolInput = None,
53
+ ) -> AsyncReceiver:
54
+ ...
55
+
56
+ @abc.abstractmethod
57
+ def set_capture_sink(self, capture_sink: CaptureSink) -> None:
58
+ ...