oscparser 1.1.0__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.
oscparser/__init__.py ADDED
@@ -0,0 +1,70 @@
1
+ """oscparser - Open Sound Control (OSC) 1.0/1.1 parser library.
2
+
3
+ This module provides encoding and decoding for OSC packets with support for:
4
+ - OSC 1.0 (UDP and TCP with length-prefixed framing)
5
+ - OSC 1.1 (TCP with SLIP framing)
6
+
7
+ Basic usage:
8
+ >>> from oscparser import OSCEncoder, OSCDecoder, OSCMessage, OSCInt, OSCModes, OSCFraming
9
+ >>> encoder = OSCEncoder(OSCModes.UDP, OSCFraming.OSC10)
10
+ >>> decoder = OSCDecoder(OSCModes.UDP, OSCFraming.OSC10)
11
+ >>> msg = OSCMessage(address="/test", args=(OSCInt(42),))
12
+ >>> encoded = encoder.encode(msg)
13
+ >>> decoded = list(decoder.feed(encoded))[0]
14
+ """
15
+
16
+ from oscparser.decode import OSCDecoder
17
+ from oscparser.encode import OSCEncoder, OSCFraming, OSCModes
18
+ from oscparser.types import (
19
+ OSC_IMPULSE,
20
+ OSCRGBA,
21
+ OSCArg,
22
+ OSCArray,
23
+ OSCAtomic,
24
+ OSCBlob,
25
+ OSCBundle,
26
+ OSCChar,
27
+ OSCDouble,
28
+ OSCFalse,
29
+ OSCFloat,
30
+ OSCImpulse,
31
+ OSCInt,
32
+ OSCInt64,
33
+ OSCMessage,
34
+ OSCMidi,
35
+ OSCNil,
36
+ OSCPacket,
37
+ OSCString,
38
+ OSCSymbol,
39
+ OSCTimeTag,
40
+ OSCTrue,
41
+ )
42
+
43
+ __all__ = [
44
+ "OSCRGBA",
45
+ "OSC_IMPULSE",
46
+ "OSCArg",
47
+ "OSCArray",
48
+ "OSCAtomic",
49
+ "OSCBlob",
50
+ "OSCBundle",
51
+ "OSCChar",
52
+ "OSCDecoder",
53
+ "OSCDouble",
54
+ "OSCEncoder",
55
+ "OSCFalse",
56
+ "OSCFloat",
57
+ "OSCFraming",
58
+ "OSCImpulse",
59
+ "OSCInt",
60
+ "OSCInt64",
61
+ "OSCMessage",
62
+ "OSCMidi",
63
+ "OSCModes",
64
+ "OSCNil",
65
+ "OSCPacket",
66
+ "OSCString",
67
+ "OSCSymbol",
68
+ "OSCTimeTag",
69
+ "OSCTrue",
70
+ ]
oscparser/ctx.py ADDED
@@ -0,0 +1,17 @@
1
+ class DataBuffer:
2
+ def __init__(self, data: bytes):
3
+ self.data = data
4
+
5
+ def startswith(self, prefix: bytes) -> bool:
6
+ return self.data.startswith(prefix)
7
+
8
+ def read(self, n: int) -> bytes:
9
+ chunk = self.data[:n]
10
+ self.data = self.data[n:]
11
+ return chunk
12
+
13
+ def write(self, data: bytes) -> None:
14
+ self.data += data
15
+
16
+ def remaining(self) -> int:
17
+ return len(self.data)
oscparser/decode.py ADDED
@@ -0,0 +1,82 @@
1
+ from typing import Iterator, cast
2
+
3
+ from oscparser.ctx import DataBuffer
4
+ from oscparser.encode import OSCFraming, OSCModes
5
+ from oscparser.framing.framer import Framer
6
+ from oscparser.framing.fullframer import FullFramer
7
+ from oscparser.framing.osc10 import OSC10Framer
8
+ from oscparser.framing.osc11 import OSC11Framer
9
+ from oscparser.processing.osc.handlers import register_osc_handlers
10
+ from oscparser.processing.osc.processing import OSCDispatcher
11
+ from oscparser.types import OSCPacket
12
+
13
+
14
+ class OSCDecoder:
15
+ """Decoder for OSC packets."""
16
+
17
+ def __init__(self, mode: OSCModes, framing: OSCFraming):
18
+ """Initialize the decoder.
19
+
20
+ Args:
21
+ mode: Transport mode, either 'udp' or 'tcp'
22
+ framing: Framing type, either 'osc10' or 'osc11'
23
+ """
24
+ self.framer = self.get_framer(mode, framing)
25
+ self.decoder = self.get_decoder()
26
+
27
+ @staticmethod
28
+ def get_decoder() -> OSCDispatcher:
29
+ """Get an OSCDispatcher configured with standard handlers.
30
+
31
+ Returns:
32
+ An OSCDispatcher instance with registered handlers
33
+ """
34
+ dispatcher = OSCDispatcher()
35
+ register_osc_handlers(dispatcher)
36
+ return dispatcher
37
+
38
+ @staticmethod
39
+ def get_framer(mode: OSCModes, framing: OSCFraming) -> Framer:
40
+ """Get the appropriate framer class based on mode and framing.
41
+
42
+ Args:
43
+ mode: Transport mode (UDP or TCP)
44
+ framing: Framing type (OSC10 or OSC11)
45
+
46
+ Returns:
47
+ The corresponding framer instance
48
+ """
49
+ if mode == OSCModes.UDP:
50
+ return FullFramer()
51
+ elif mode == OSCModes.TCP:
52
+ if framing == OSCFraming.OSC10:
53
+ return OSC10Framer()
54
+ elif framing == OSCFraming.OSC11:
55
+ return OSC11Framer()
56
+ raise ValueError("Unsupported mode or framing type")
57
+
58
+ def decode(self, data: bytes) -> Iterator[OSCPacket]:
59
+ """Feed data into the decoder and yield decoded OSC packets.
60
+
61
+ For streaming TCP connections where data arrives in chunks.
62
+
63
+ Args:
64
+ data: Raw bytes received from socket
65
+
66
+ Yields:
67
+ Decoded OSC packets
68
+ """
69
+ # Unframe the data to get complete OSC packets
70
+ for packet_data in self.framer.feed(data):
71
+ # Decode each complete packet
72
+ data_buffer = DataBuffer(packet_data)
73
+ handler = self.decoder.get_handler(data_buffer)
74
+ yield cast(OSCPacket, handler.decode(data_buffer))
75
+
76
+ def clear_buffer(self) -> None:
77
+ """Clear the internal framer buffer.
78
+
79
+ Useful when resetting the connection or recovering from errors.
80
+ """
81
+ if hasattr(self.framer, "clear_buffer"):
82
+ self.framer.clear_buffer()
oscparser/encode.py ADDED
@@ -0,0 +1,78 @@
1
+ from enum import Enum
2
+
3
+ from oscparser.ctx import DataBuffer
4
+ from oscparser.framing.framer import Framer
5
+ from oscparser.framing.fullframer import FullFramer
6
+ from oscparser.framing.osc10 import OSC10Framer
7
+ from oscparser.framing.osc11 import OSC11Framer
8
+ from oscparser.processing.osc.handlers import register_osc_handlers
9
+ from oscparser.processing.osc.processing import OSCDispatcher
10
+ from oscparser.types import OSCPacket
11
+
12
+
13
+ class OSCModes(Enum):
14
+ UDP = "udp"
15
+ TCP = "tcp"
16
+
17
+
18
+ class OSCFraming(Enum):
19
+ OSC10 = "osc10"
20
+ OSC11 = "osc11"
21
+
22
+
23
+ class OSCEncoder:
24
+ """Encoder for OSC packets."""
25
+
26
+ def __init__(self, mode: OSCModes, framing: OSCFraming):
27
+ """Initialize the framer.
28
+
29
+ Args:
30
+ mode: Transport mode, either 'udp' or 'tcp'
31
+ """
32
+ self.framer = self.get_framer(mode, framing)
33
+ self.encoder = self.get_encoder()
34
+
35
+ @staticmethod
36
+ def get_encoder() -> OSCDispatcher:
37
+ """Get an OSCDispatcher configured with standard handlers.
38
+
39
+ Returns:
40
+ An OSCDispatcher instance with registered handlers
41
+ """
42
+ dispatcher = OSCDispatcher()
43
+ register_osc_handlers(dispatcher)
44
+ return dispatcher
45
+
46
+ @staticmethod
47
+ def get_framer(mode: OSCModes, framing: OSCFraming) -> Framer:
48
+ """Get the appropriate framer class based on mode and framing.
49
+
50
+ Args:
51
+ mode: Transport mode (UDP or TCP)
52
+ framing: Framing type (OSC10 or OSC11)
53
+
54
+ Returns:
55
+ The corresponding framer class
56
+ """
57
+ if mode == OSCModes.UDP:
58
+ return FullFramer()
59
+ elif mode == OSCModes.TCP:
60
+ if framing == OSCFraming.OSC10:
61
+ return OSC10Framer()
62
+ elif framing == OSCFraming.OSC11:
63
+ return OSC11Framer()
64
+ raise ValueError("Unsupported mode or framing type")
65
+
66
+ def encode(self, packet: OSCPacket) -> bytes:
67
+ """Encode and frame an OSC packet.
68
+
69
+ Args:
70
+ packet: The OSC packet to encode
71
+ Returns:
72
+ Framed OSC packet bytes
73
+ """
74
+ data_buffer = DataBuffer(b"")
75
+ handler = self.encoder.get_object_handler(type(packet))
76
+ handler.encode(packet, data_buffer)
77
+ framed_packet = self.framer.frame(data_buffer.data)
78
+ return framed_packet
@@ -0,0 +1,6 @@
1
+ """OSC framing support for OSC 1.0 (UDP) and OSC 1.1 (TCP/SLIP)."""
2
+
3
+ from oscparser.framing.osc10 import OSC10Framer
4
+ from oscparser.framing.osc11 import OSC11Framer
5
+
6
+ __all__ = ["OSC10Framer", "OSC11Framer"]
@@ -0,0 +1,18 @@
1
+ from typing import Iterator, Protocol
2
+
3
+
4
+ class Framer(Protocol):
5
+ """Protocol for OSC framers."""
6
+
7
+ @staticmethod
8
+ def frame(packet: bytes) -> bytes:
9
+ """Frame an OSC packet for transport."""
10
+ ...
11
+
12
+ def feed(self, data: bytes) -> Iterator[bytes]:
13
+ """Feed data into the framer and yield complete packets."""
14
+ ...
15
+
16
+ def clear_buffer(self) -> None:
17
+ """Clear the internal receive buffer."""
18
+ ...
@@ -0,0 +1,42 @@
1
+ from typing import Iterator
2
+
3
+ from oscparser.framing.framer import Framer
4
+
5
+
6
+ class FullFramer(Framer):
7
+ """A framer that does not perform any framing.
8
+
9
+ This class is useful for protocols that do not require framing,
10
+ such as UDP transport of OSC packets.
11
+
12
+ Complies with the Framer protocol.
13
+ """
14
+
15
+ @staticmethod
16
+ def frame(packet: bytes) -> bytes:
17
+ """Return the packet as-is without any framing.
18
+
19
+ Args:
20
+ packet: Raw OSC packet bytes (message or bundle)
21
+
22
+ Returns:
23
+ The same packet bytes without any framing
24
+ """
25
+ return packet
26
+
27
+ def feed(self, data: bytes) -> Iterator[bytes]:
28
+ """Yield the incoming data as a complete packet.
29
+
30
+ Since there is no framing, each call to feed yields the entire data.
31
+
32
+ Args:
33
+ data: Raw bytes received from transport
34
+
35
+ Yields:
36
+ The entire data as a single OSC packet
37
+ """
38
+ yield data
39
+
40
+ def clear_buffer(self) -> None:
41
+ """No internal buffer to clear in this framer."""
42
+ pass
@@ -0,0 +1,70 @@
1
+ """OSC 1.0 framing for TCP transport.
2
+
3
+ OSC 1.0 over TCP uses length-prefixed framing: a 4-byte big-endian
4
+ integer indicating the packet size, followed by the packet data.
5
+ """
6
+
7
+ import struct
8
+ from typing import Iterator
9
+
10
+ from oscparser.framing.framer import Framer
11
+
12
+
13
+ class OSC10Framer(Framer):
14
+ """Framer for OSC 1.0 packets over TCP.
15
+
16
+ Each packet is prefixed with a 4-byte big-endian size header.
17
+ Complies with the Framer protocol.
18
+ """
19
+
20
+ def __init__(self):
21
+ """Initialize the framer with an empty receive buffer."""
22
+ self._buffer = bytearray()
23
+
24
+ @staticmethod
25
+ def frame(packet: bytes) -> bytes:
26
+ """Frame an OSC packet for TCP transport.
27
+
28
+ OSC 1.0 over TCP uses length-prefixed framing: a 4-byte big-endian
29
+ integer indicating the packet size, followed by the packet data.
30
+
31
+ Args:
32
+ packet: Raw OSC packet bytes (message or bundle)
33
+
34
+ Returns:
35
+ Length-prefixed packet (4 bytes size + packet data)
36
+ """
37
+ size = len(packet)
38
+ return struct.pack(">I", size) + packet
39
+
40
+ def feed(self, data: bytes) -> Iterator[bytes]:
41
+ """Feed data into the framer and yield complete packets.
42
+
43
+ Buffers partial packets and yields complete packets as they arrive.
44
+
45
+ Args:
46
+ data: Raw bytes received from TCP socket
47
+
48
+ Yields:
49
+ Complete OSC packets (without size prefix)
50
+ """
51
+ self._buffer.extend(data)
52
+
53
+ while len(self._buffer) >= 4:
54
+ # Read the size prefix
55
+ size = struct.unpack(">I", bytes(self._buffer[:4]))[0]
56
+
57
+ # Check if we have the complete packet
58
+ if len(self._buffer) < 4 + size:
59
+ # Not enough data yet
60
+ break
61
+
62
+ # Extract the packet
63
+ packet = bytes(self._buffer[4 : 4 + size])
64
+ self._buffer = self._buffer[4 + size :]
65
+
66
+ yield packet
67
+
68
+ def clear_buffer(self) -> None:
69
+ """Clear the internal receive buffer."""
70
+ self._buffer.clear()
@@ -0,0 +1,208 @@
1
+ """OSC 1.1 framing for TCP transport using SLIP encoding.
2
+
3
+ OSC 1.1 adds support for stream-oriented transports like TCP. Since TCP is a
4
+ byte stream without inherent message boundaries, SLIP (Serial Line Internet
5
+ Protocol) encoding is used to frame OSC packets.
6
+
7
+ SLIP uses special bytes to delimit packets:
8
+ - END (0xC0): Packet boundary marker
9
+ - ESC (0xDB): Escape byte
10
+ - ESC_END (0xDC): Escaped END byte
11
+ - ESC_ESC (0xDD): Escaped ESC byte
12
+
13
+ Reference: RFC 1055, OSC 1.1 specification
14
+ """
15
+
16
+ from typing import Iterator
17
+
18
+ from oscparser.framing.framer import Framer
19
+
20
+ # SLIP protocol constants
21
+ END = b"\xc0"
22
+ ESC = b"\xdb"
23
+ ESC_END = b"\xdc"
24
+ ESC_ESC = b"\xdd"
25
+
26
+
27
+ class SLIPError(ValueError):
28
+ """Exception raised when SLIP protocol violations are detected."""
29
+
30
+ pass
31
+
32
+
33
+ class OSC11Framer(Framer):
34
+ """Framer for OSC 1.1 packets over TCP using SLIP encoding.
35
+
36
+ This class handles:
37
+ - Encoding OSC packets with SLIP framing for transmission
38
+ - Decoding SLIP-framed packets from received byte streams
39
+ - Buffering partial packets in stream-based reception
40
+
41
+ Complies with the Framer protocol.
42
+ """
43
+
44
+ def __init__(self):
45
+ """Initialize the framer with an empty receive buffer."""
46
+ self._buffer = bytearray()
47
+
48
+ @staticmethod
49
+ def frame(packet: bytes) -> bytes:
50
+ """Frame an OSC packet for TCP transport using SLIP encoding.
51
+
52
+ The packet is escaped according to SLIP rules and wrapped with END bytes.
53
+
54
+ SLIP encoding rules:
55
+ - Replace ESC with ESC + ESC_ESC
56
+ - Replace END with ESC + ESC_END
57
+ - Wrap with END bytes (before and after)
58
+
59
+ Args:
60
+ packet: Raw OSC packet bytes (message or bundle)
61
+
62
+ Returns:
63
+ SLIP-encoded packet ready for transmission
64
+ """
65
+ if not packet:
66
+ packet = b""
67
+
68
+ # Escape special bytes
69
+ encoded = packet.replace(ESC, ESC + ESC_ESC)
70
+ encoded = encoded.replace(END, ESC + ESC_END)
71
+
72
+ # Wrap with END bytes
73
+ return END + encoded + END
74
+
75
+ @staticmethod
76
+ def _unframe(slip_packet: bytes) -> bytes:
77
+ """Unframe a single SLIP-encoded packet.
78
+
79
+ This decodes one complete SLIP packet. The packet must be a complete,
80
+ properly framed SLIP packet (starting and ending with END bytes).
81
+
82
+ Args:
83
+ slip_packet: SLIP-encoded packet bytes
84
+
85
+ Returns:
86
+ Decoded OSC packet bytes
87
+
88
+ Raises:
89
+ SLIPError: If the packet is malformed or contains invalid sequences
90
+ """
91
+ if not OSC11Framer._is_valid_slip(slip_packet):
92
+ raise SLIPError(f"Invalid SLIP packet: {slip_packet!r}")
93
+
94
+ # Strip END bytes
95
+ decoded = slip_packet.strip(END)
96
+
97
+ # Replace escaped sequences
98
+ decoded = decoded.replace(ESC + ESC_END, END)
99
+ decoded = decoded.replace(ESC + ESC_ESC, ESC)
100
+
101
+ return decoded
102
+
103
+ def feed(self, data: bytes) -> Iterator[bytes]:
104
+ """Feed data into the framer and yield complete packets.
105
+
106
+ This method maintains an internal buffer to handle partial packets.
107
+ As data arrives, it's buffered until complete SLIP-framed packets
108
+ can be extracted.
109
+
110
+ Args:
111
+ data: Raw bytes received from TCP socket
112
+
113
+ Yields:
114
+ Complete OSC packets (SLIP-decoded)
115
+
116
+ Raises:
117
+ SLIPError: If malformed SLIP sequences are detected
118
+ """
119
+ self._buffer.extend(data)
120
+
121
+ while True:
122
+ # If buffer doesn't start with END, find first END and discard garbage
123
+ try:
124
+ first_end = self._buffer.index(END[0])
125
+ # Discard everything before the first END as garbage/misalignment
126
+ self._buffer = self._buffer[first_end:]
127
+ except ValueError:
128
+ # No END byte found, clear buffer and wait for more data
129
+ break
130
+
131
+ # Now buffer starts with END, find the closing END
132
+ if len(self._buffer) < 2:
133
+ # Need at least 2 bytes to have a complete packet
134
+ break
135
+
136
+ try:
137
+ # Search for closing END (starting from position 1)
138
+ closing_end = self._buffer.index(END[0], 1)
139
+ except ValueError:
140
+ # No closing END found yet, wait for more data
141
+ break
142
+
143
+ # Extract the complete SLIP packet (including both END bytes)
144
+ slip_packet = bytes(self._buffer[: closing_end + 1])
145
+ self._buffer = self._buffer[closing_end + 1 :]
146
+
147
+ # Skip empty packets (double END bytes - these are packet separators)
148
+ if slip_packet == END + END:
149
+ # Add an END back to the buffer to maintain alignment
150
+ self._buffer.insert(0, END[0])
151
+ continue
152
+
153
+ # Decode and yield the packet
154
+ try:
155
+ packet = self._unframe(slip_packet)
156
+ if packet: # Only yield non-empty packets
157
+ yield packet
158
+ except SLIPError:
159
+ # Skip malformed packets and continue processing
160
+ continue
161
+
162
+ def clear_buffer(self) -> None:
163
+ """Clear the internal receive buffer.
164
+
165
+ Useful when resetting the connection or recovering from errors.
166
+ """
167
+ self._buffer.clear()
168
+
169
+ @staticmethod
170
+ def _is_valid_slip(packet: bytes) -> bool:
171
+ """Check if a packet is valid according to SLIP protocol.
172
+
173
+ A valid SLIP packet:
174
+ - Contains no unescaped END bytes except at boundaries
175
+ - Each ESC byte is followed by ESC_END or ESC_ESC
176
+ - Does not end with a trailing ESC byte
177
+
178
+ Args:
179
+ packet: SLIP packet to validate
180
+
181
+ Returns:
182
+ True if valid, False otherwise
183
+ """
184
+ # Strip boundary END bytes
185
+ inner = packet.strip(END)
186
+
187
+ # Check for unescaped END bytes in the middle
188
+ if END[0] in inner:
189
+ return False
190
+
191
+ # Check for trailing ESC
192
+ if inner.endswith(ESC):
193
+ return False
194
+
195
+ # Check that all ESC bytes are properly followed
196
+ i = 0
197
+ while i < len(inner):
198
+ if inner[i : i + 1] == ESC:
199
+ if i + 1 >= len(inner):
200
+ return False # ESC at end
201
+ next_byte = inner[i + 1 : i + 2]
202
+ if next_byte not in (ESC_END, ESC_ESC):
203
+ return False # Invalid escape sequence
204
+ i += 2
205
+ else:
206
+ i += 1
207
+
208
+ return True