├── .gitignore ├── LICENSE ├── README.rst ├── __init__.py ├── gotalk ├── __init__.py ├── exceptions.py └── protocol │ ├── __init__.py │ ├── defines.py │ ├── messages.py │ └── version01 │ ├── __init__.py │ └── messages.py ├── requirements ├── install.txt └── test.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py └── test_version01.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .eggs 3 | *.pyc 4 | *.swp 5 | *.tmp 6 | *.log 7 | .DS_Store 8 | *.pid 9 | settings.py 10 | build 11 | dist 12 | *.egg-info 13 | MANIFEST 14 | doc_src/_build 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Gregory Taylor 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-gotalk 2 | ============= 3 | 4 | :Author: Greg Taylor 5 | :License: BSD 6 | :Status: Early development, Stalled (gotalk dev itself seems to have ceased) 7 | :Gotalk Version: 01 8 | 9 | python-gotalk aspires to be a Python implementation of the Gotalk_ protocol. 10 | It is currently in the early goings, so you probably don't want to use this 11 | for anything serious. 12 | 13 | As of this second, this module contains everything needed to form or parse 14 | gotalk messages. Unlike the Go reference implementation, we don't have any 15 | convenience stuff to tie it all together and make it work... yet. 16 | 17 | As noted above, python-gotalk currently supports only protocol version 01. 18 | Given that gotalk 01 is still evolving, this is a bit of a moving target 19 | currently. 20 | 21 | Documentation 22 | ------------- 23 | 24 | Coming soon, hopefully! 25 | 26 | License 27 | ------- 28 | 29 | This project, and all contributed code, are licensed under the BSD License. 30 | A copy of the BSD License may be found in the repository. 31 | 32 | .. _Gotalk: https://github.com/rsms/gotalk 33 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtaylor/python-gotalk/7c26eaa514605490ad2b254a8fb2b0e05d652d45/__init__.py -------------------------------------------------------------------------------- /gotalk/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | VERSION = (0, 1, 0) 4 | __version__ = '.'.join(map(str, VERSION)) + '-dev' 5 | -------------------------------------------------------------------------------- /gotalk/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | General, non-protocol-version-specific exceptions. 3 | """ 4 | 5 | 6 | class InvalidProtocolVersionError(Exception): 7 | """ 8 | Raised when encountering a protocol version that we don't understand, or 9 | in the presence of a malformed version exchange. 10 | """ 11 | 12 | pass 13 | 14 | 15 | class InvalidMessageTypeIDError(Exception): 16 | """ 17 | Raised when an invalid message type ID prefix is encountered. This is 18 | usually due to a malformed message, or we're dealing with a different 19 | version of the protocol that we don't understand. 20 | """ 21 | 22 | pass 23 | 24 | 25 | class InvalidOperationError(Exception): 26 | """ 27 | Raised when a operation is somehow invalid. Also serves as a base class for 28 | other, more specific operation errors. 29 | """ 30 | 31 | pass 32 | 33 | 34 | class OperationTooLongError(InvalidOperationError): 35 | """ 36 | Raised when trying to send a message whose operation exceeds the 37 | operation limit. 38 | """ 39 | 40 | pass 41 | 42 | 43 | class InvalidPayloadError(Exception): 44 | """ 45 | Raised when a payload is somehow invalid. Also serves as a base class for 46 | other, more specific payload errors. 47 | """ 48 | 49 | pass 50 | 51 | 52 | class PayloadTooLongError(InvalidPayloadError): 53 | """ 54 | Raised when trying to send a message whose payload exceeds the 55 | payload limit. 56 | """ 57 | 58 | pass 59 | -------------------------------------------------------------------------------- /gotalk/protocol/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'gtaylor' 2 | -------------------------------------------------------------------------------- /gotalk/protocol/defines.py: -------------------------------------------------------------------------------- 1 | """ 2 | Constants and other defines. 3 | """ 4 | 5 | # Various message type characters. These are the first byte in every message 6 | # but the initial version exchange. 7 | SINGLE_REQUEST_TYPE = "r" 8 | SINGLE_RESULT_TYPE = "R" 9 | STREAM_REQUEST_TYPE = "s" 10 | STREAM_REQUEST_PART_TYPE = "p" 11 | STREAM_RESULT_TYPE = "S" 12 | ERROR_RESULT_TYPE = "E" 13 | PROTOCOL_ERROR_TYPE = "f" 14 | RETRY_RESULT_TYPE = "e" 15 | NOTIFICATION_TYPE = "n" 16 | 17 | # Maps the single-character message type ID to some standardized class names 18 | # that we use for all versions of the protocol. 19 | MESSAGE_TYPE_TO_CLASS_MAP = { 20 | SINGLE_REQUEST_TYPE: 'SingleRequestMessage', 21 | SINGLE_RESULT_TYPE: 'SingleResultMessage', 22 | STREAM_REQUEST_TYPE: 'StreamRequestMessage', 23 | STREAM_REQUEST_PART_TYPE: 'StreamRequestPartMessage', 24 | STREAM_RESULT_TYPE: 'StreamResultMessage', 25 | ERROR_RESULT_TYPE: 'ErrorResultMessage', 26 | PROTOCOL_ERROR_TYPE: 'ProtocolErrorMessage', 27 | RETRY_RESULT_TYPE: 'RetryResultMessage', 28 | NOTIFICATION_TYPE: 'NotificationMessage', 29 | } 30 | -------------------------------------------------------------------------------- /gotalk/protocol/messages.py: -------------------------------------------------------------------------------- 1 | """ 2 | The top-level API for Gotalk message marshalling/unmarshalling. 3 | """ 4 | 5 | from gotalk.exceptions import InvalidMessageTypeIDError, \ 6 | InvalidProtocolVersionError 7 | from gotalk.protocol import version01 8 | from gotalk.protocol.defines import MESSAGE_TYPE_TO_CLASS_MAP 9 | 10 | PROTOCOL_VERSION_MAP = { 11 | "01": version01, 12 | } 13 | 14 | 15 | def read_version_message(m_bytes): 16 | """ 17 | Communication alway starts with an exchange of the protocol version. 18 | Hopefully this won't ever change... 19 | 20 | :param str m_bytes: The byte string to parse. 21 | :rtype: int 22 | :returns: The version number shared in the message. 23 | :raises: InvalidProtocolVersionError if the version is mal-formed. 24 | """ 25 | 26 | if len(m_bytes) != 2: 27 | raise InvalidProtocolVersionError() 28 | version_str = m_bytes[0:2] 29 | return version_str 30 | 31 | 32 | def read_message(m_bytes, proto_version): 33 | """ 34 | Parses a messages, spitting out a properly formed instance of the 35 | appropriate ``GotalkMessage`` sub-class. 36 | 37 | :param str m_bytes: The unmodified m_bytes to parse. 38 | :param str proto_version: The protocol version to use in the exchange. 39 | :rtype: GotalkMessage 40 | :returns: One of the ``GotalkMessage` sub-class. 41 | :raises: InvalidProtocolVersionError if we don't know how to handle 42 | the encountered version. 43 | """ 44 | 45 | # This is the sub-module for the specified proto version. 46 | try: 47 | proto_module = PROTOCOL_VERSION_MAP[proto_version] 48 | except KeyError: 49 | # TODO: Depending on the backwards-compatibility policy with gotalk, 50 | # we might be able to fall back to the latest known version and 51 | # potentially limp along. Too early to know. 52 | raise InvalidProtocolVersionError("Invalid gotalk protocol version.") 53 | 54 | type_id = m_bytes[0] 55 | try: 56 | msg_class_name = MESSAGE_TYPE_TO_CLASS_MAP[type_id] 57 | except KeyError: 58 | raise InvalidMessageTypeIDError() 59 | msg_class = getattr(proto_module, msg_class_name) 60 | return msg_class.from_bytes(m_bytes) 61 | 62 | 63 | def write_message(message): 64 | """ 65 | Given a message, dump it to bytes. 66 | 67 | :param message: An instance of a GotalkMessage child. 68 | :rtype: str 69 | :returns: The bytes to send for the given message. 70 | """ 71 | 72 | return message.to_bytes() 73 | -------------------------------------------------------------------------------- /gotalk/protocol/version01/__init__.py: -------------------------------------------------------------------------------- 1 | from . messages import GotalkMessage, GotalkRequestMessage, GotalkResultMessage, \ 2 | ProtocolVersionMessage, SingleRequestMessage, SingleResultMessage, \ 3 | StreamRequestMessage, StreamRequestPartMessage, StreamResultMessage, \ 4 | ErrorResultMessage, RetryResultMessage, NotificationMessage, \ 5 | ProtocolErrorMessage 6 | -------------------------------------------------------------------------------- /gotalk/protocol/version01/messages.py: -------------------------------------------------------------------------------- 1 | """ 2 | Version 00 message marshalling/unmarshalling. 3 | """ 4 | 5 | from gotalk.exceptions import PayloadTooLongError, OperationTooLongError 6 | from gotalk.protocol.defines import SINGLE_REQUEST_TYPE, SINGLE_RESULT_TYPE, \ 7 | STREAM_REQUEST_TYPE, STREAM_REQUEST_PART_TYPE, STREAM_RESULT_TYPE, \ 8 | ERROR_RESULT_TYPE, NOTIFICATION_TYPE, RETRY_RESULT_TYPE, PROTOCOL_ERROR_TYPE 9 | 10 | 11 | class GotalkMessage(object): 12 | """ 13 | .. tip:: Don't use this class directly! 14 | """ 15 | 16 | protocol_version = "01" 17 | payload_max_length = 4294967295 18 | 19 | _request_id_start = 1 20 | _request_id_end = 5 21 | _payload_length_bytes = 8 22 | 23 | def _pad_request_id(self, request_id): 24 | return str(request_id).zfill(3) 25 | 26 | def _check_payload_length(self, payload): 27 | payload_length = len(payload) 28 | if payload_length > self.payload_max_length: 29 | raise PayloadTooLongError( 30 | "Payload length limit exceeded. Must be < 4 GB.") 31 | return payload_length 32 | 33 | @classmethod 34 | def _get_request_id_from_bytes(cls, m_bytes): 35 | return m_bytes[cls._request_id_start:cls._request_id_end] 36 | 37 | @classmethod 38 | def _get_payload_from_bytes(cls, m_bytes, payload_length_start): 39 | payload_length_end = payload_length_start + cls._payload_length_bytes 40 | return m_bytes[payload_length_end:] 41 | 42 | 43 | class GotalkRequestMessage(GotalkMessage): 44 | """ 45 | .. tip:: Don't use this class directly! 46 | """ 47 | 48 | operation_max_length = 4095 49 | 50 | _operation_length_bytes = 3 51 | _operation_length_start = GotalkMessage._request_id_end 52 | _operation_length_end = GotalkMessage._request_id_end + _operation_length_bytes 53 | 54 | def _check_operation_length(self, operation): 55 | operation_length = len(operation) 56 | if operation_length > self.operation_max_length: 57 | raise OperationTooLongError( 58 | "Operation length limit exceeded. Must be < 4 KB.") 59 | return operation_length 60 | 61 | @classmethod 62 | def _get_operation_from_bytes(cls, m_bytes): 63 | op_length_hex = m_bytes[cls._operation_length_start:cls._operation_length_end] 64 | operation_length = int(op_length_hex, 16) 65 | operation_end = cls._operation_length_end + operation_length 66 | operation = m_bytes[cls._operation_length_end: operation_end] 67 | return operation, operation_end 68 | 69 | 70 | class GotalkResultMessage(GotalkMessage): 71 | """ 72 | .. tip:: Don't use this class directly! 73 | """ 74 | 75 | pass 76 | 77 | 78 | class ProtocolVersionMessage(GotalkMessage): 79 | """ 80 | ProtocolVersion = 81 | """ 82 | 83 | def to_bytes(self): 84 | return self.protocol_version 85 | 86 | 87 | class ProtocolErrorMessage(GotalkMessage): 88 | """ 89 | ProtocolError = "f" code 90 | 91 | code = hexUInt8 92 | 93 | Code values 94 | ----------- 95 | 1 Unsupported protocol version 96 | 2 Invalid message 97 | 98 | +---------------------- Protocol Error 99 | | +---------------- code 1 100 | | | 101 | | | 102 | | | 103 | f00000001 104 | """ 105 | 106 | type_id = PROTOCOL_ERROR_TYPE 107 | 108 | _code_bytes = 8 109 | _code_start = 1 110 | _code_end = _code_start + _code_bytes 111 | 112 | def __init__(self, code): 113 | self.code = code 114 | 115 | def to_bytes(self): 116 | return "{type_id}{code:08x}".format(type_id=self.type_id, code=self.code) 117 | 118 | @classmethod 119 | def from_bytes(cls, m_bytes): 120 | code = int(m_bytes[cls._code_start:cls._code_end], 16) 121 | return cls(code) 122 | 123 | 124 | class SingleRequestMessage(GotalkRequestMessage): 125 | """ 126 | SingleRequest = "r" requestID operation payload 127 | 128 | requestID = 129 | operation = text3 130 | payload = payloadSize payloadData? 131 | payloadSize = hexUInt8 132 | payloadData = {payloadSize} 133 | 134 | text3 = text3Size text3Value 135 | text3Size = hexUInt3 136 | text3Value = <{text3Size} as utf8 text> 137 | 138 | +------------------ SingleRequest 139 | | +---------------- requestID "0001" 140 | | | +------------- text3Size 4 141 | | | | +--------- operation "echo" 142 | | | | | +- payloadSize 25 143 | | | | | | 144 | r0001004echo00000019{"message":"Hello World"} 145 | """ 146 | 147 | type_id = SINGLE_REQUEST_TYPE 148 | 149 | def __init__(self, request_id, operation, payload): 150 | self.request_id = request_id 151 | self.operation = operation 152 | self.payload = payload 153 | 154 | def to_bytes(self): 155 | operation_length = self._check_operation_length(self.operation) 156 | payload_length = self._check_payload_length(self.payload) 157 | return "{type_id}{request_id}" \ 158 | "{operation_length:03d}{operation}" \ 159 | "{payload_length:08x}{payload}".format( 160 | type_id=self.type_id, 161 | request_id=self._pad_request_id(self.request_id), 162 | operation_length=operation_length, operation=self.operation, 163 | payload_length=payload_length, payload=self.payload) 164 | 165 | @classmethod 166 | def from_bytes(cls, m_bytes): 167 | request_id = cls._get_request_id_from_bytes(m_bytes) 168 | operation, operation_end = cls._get_operation_from_bytes(m_bytes) 169 | payload = cls._get_payload_from_bytes(m_bytes, operation_end) 170 | return cls(request_id, operation, payload) 171 | 172 | 173 | class SingleResultMessage(GotalkResultMessage): 174 | """ 175 | SingleResult = "R" requestID payload 176 | 177 | requestID = 178 | payload = payloadSize payloadData? 179 | payloadSize = hexUInt8 180 | payloadData = {payloadSize} 181 | 182 | +----------------- SingleResult 183 | | +--------------- requestID "0001" 184 | | | +------- payloadSize 25 185 | | | | 186 | R000100000019{"message":"Hello World"} 187 | """ 188 | 189 | type_id = SINGLE_RESULT_TYPE 190 | 191 | def __init__(self, request_id, payload): 192 | self.request_id = request_id 193 | self.payload = payload 194 | 195 | def to_bytes(self): 196 | payload_length = self._check_payload_length(self.payload) 197 | return "{type_id}{request_id}{payload_length:08x}{payload}".format( 198 | type_id=self.type_id, 199 | request_id=self._pad_request_id(self.request_id), 200 | payload_length=payload_length, payload=self.payload) 201 | 202 | @classmethod 203 | def from_bytes(cls, m_bytes): 204 | request_id = cls._get_request_id_from_bytes(m_bytes) 205 | payload = cls._get_payload_from_bytes( 206 | m_bytes, payload_length_start=cls._request_id_end) 207 | return cls(request_id, payload) 208 | 209 | 210 | class StreamRequestMessage(GotalkRequestMessage): 211 | """ 212 | StreamRequest = "s" requestID operation payload StreamReqPart+ 213 | 214 | requestID = 215 | operation = text3 216 | payload = payloadSize payloadData? 217 | payloadSize = hexUInt8 218 | payloadData = {payloadSize} 219 | StreamReqPart = "p" requestID payload 220 | 221 | text3 = text3Size text3Value 222 | text3Size = hexUInt3 223 | text3Value = <{text3Size} as utf8 text> 224 | 225 | +------------------ StreamRequest 226 | | +---------------- requestID "0001" 227 | | | +--------- operation "echo" (text3Size 4, text3Value "echo") 228 | | | | +- payloadSize 25 229 | | | | | 230 | s0001004echo0000000b{"message": 231 | """ 232 | 233 | type_id = STREAM_REQUEST_TYPE 234 | 235 | def __init__(self, request_id, operation, payload): 236 | self.request_id = request_id 237 | self.operation = operation 238 | self.payload = payload 239 | 240 | def to_bytes(self): 241 | operation_length = self._check_operation_length(self.operation) 242 | payload_length = self._check_payload_length(self.payload) 243 | return "{type_id}{request_id}" \ 244 | "{operation_length:03d}{operation}" \ 245 | "{payload_length:08x}{payload}".format( 246 | type_id=self.type_id, 247 | request_id=self._pad_request_id(self.request_id), 248 | operation_length=operation_length, operation=self.operation, 249 | payload_length=payload_length, payload=self.payload) 250 | 251 | @classmethod 252 | def from_bytes(cls, m_bytes): 253 | request_id = cls._get_request_id_from_bytes(m_bytes) 254 | operation, operation_end = cls._get_operation_from_bytes(m_bytes) 255 | payload = cls._get_payload_from_bytes(m_bytes, operation_end) 256 | return cls(request_id, operation, payload) 257 | 258 | 259 | class StreamRequestPartMessage(GotalkRequestMessage): 260 | """ 261 | StreamReqPart = "p" requestID payload 262 | 263 | requestID = 264 | payload = payloadSize payloadData? 265 | payloadSize = hexUInt8 266 | payloadData = {payloadSize} 267 | 268 | +------------------ streamReqPart 269 | | +---------------- requestID "0001" 270 | | | +-------- payloadSize 25 271 | | | | 272 | p00010000000e"Hello World"} 273 | """ 274 | 275 | type_id = STREAM_REQUEST_PART_TYPE 276 | 277 | def __init__(self, request_id, payload): 278 | self.request_id = request_id 279 | self.payload = payload 280 | 281 | def to_bytes(self): 282 | payload_length = self._check_payload_length(self.payload) 283 | return "{type_id}{request_id}{payload_length:08x}{payload}".format( 284 | type_id=self.type_id, 285 | request_id=self._pad_request_id(self.request_id), 286 | payload_length=payload_length, payload=self.payload) 287 | 288 | @classmethod 289 | def from_bytes(cls, m_bytes): 290 | request_id = cls._get_request_id_from_bytes(m_bytes) 291 | payload = cls._get_payload_from_bytes( 292 | m_bytes, payload_length_start=cls._request_id_end) 293 | return cls(request_id, payload) 294 | 295 | 296 | class StreamResultMessage(GotalkResultMessage): 297 | """ 298 | StreamResult = "S" requestID payload StreamResult* 299 | 300 | requestID = 301 | payload = payloadSize payloadData? 302 | payloadSize = hexUInt8 303 | payloadData = {payloadSize} 304 | 305 | +------------------ StreamResult (1st part) 306 | | +---------------- requestID "0001" 307 | | | +-------- payloadSize 25 308 | | | | 309 | S00010000000b{"message": 310 | """ 311 | 312 | type_id = STREAM_RESULT_TYPE 313 | 314 | def __init__(self, request_id, payload): 315 | self.request_id = request_id 316 | self.payload = payload 317 | 318 | def to_bytes(self): 319 | payload_length = self._check_payload_length(self.payload) 320 | return "{type_id}{request_id}{payload_length:08x}{payload}".format( 321 | type_id=self.type_id, 322 | request_id=self._pad_request_id(self.request_id), 323 | payload_length=payload_length, payload=self.payload) 324 | 325 | @classmethod 326 | def from_bytes(cls, m_bytes): 327 | request_id = cls._get_request_id_from_bytes(m_bytes) 328 | payload = cls._get_payload_from_bytes( 329 | m_bytes, payload_length_start=cls._request_id_end) 330 | return cls(request_id, payload) 331 | 332 | 333 | class ErrorResultMessage(GotalkResultMessage): 334 | """ 335 | ErrorResult = "E" requestID payload 336 | 337 | requestID = 338 | payload = payloadSize payloadData? 339 | payloadSize = hexUInt8 340 | payloadData = {payloadSize} 341 | 342 | +------------------ ErrorResult 343 | | +---------------- requestID "0001" 344 | | | +-------- payloadSize 38 345 | | | | 346 | E000100000026{"error":"Unknown operation \"echo\""} 347 | """ 348 | 349 | type_id = ERROR_RESULT_TYPE 350 | 351 | def __init__(self, request_id, payload): 352 | self.request_id = request_id 353 | self.payload = payload 354 | 355 | def to_bytes(self): 356 | payload_length = self._check_payload_length(self.payload) 357 | return "{type_id}{request_id}{payload_length:08x}{payload}".format( 358 | type_id=self.type_id, 359 | request_id=self._pad_request_id(self.request_id), 360 | payload_length=payload_length, payload=self.payload) 361 | 362 | @classmethod 363 | def from_bytes(cls, m_bytes): 364 | request_id = cls._get_request_id_from_bytes(m_bytes) 365 | payload = cls._get_payload_from_bytes( 366 | m_bytes, payload_length_start=cls._request_id_end) 367 | return cls(request_id, payload) 368 | 369 | 370 | class RetryResultMessage(GotalkResultMessage): 371 | """ 372 | RetryResult = "e" requestID wait payload 373 | 374 | requestID = 375 | wait = hexUInt8 376 | payload = payloadSize payloadData? 377 | payloadSize = hexUInt8 378 | payloadData = {payloadSize} 379 | 380 | +-------------------- RetryResult 381 | | +------------------ requestID "0001" 382 | | | +---------- wait 0 383 | | | | +-- payloadSize 20 384 | | | | | 385 | e00010000000000000014"service restarting" 386 | """ 387 | 388 | type_id = RETRY_RESULT_TYPE 389 | _wait_bytes = 8 390 | _wait_start = GotalkRequestMessage._request_id_end 391 | _wait_end = GotalkRequestMessage._request_id_end + _wait_bytes 392 | 393 | def __init__(self, request_id, wait, payload): 394 | self.request_id = request_id 395 | if not isinstance(wait, int): 396 | raise ValueError("wait value must be an int.") 397 | self.wait = wait 398 | self.payload = payload 399 | 400 | def to_bytes(self): 401 | payload_length = self._check_payload_length(self.payload) 402 | return "{type_id}{request_id}{wait:08x}{payload_length:08x}{payload}".format( 403 | type_id=self.type_id, 404 | request_id=self._pad_request_id(self.request_id), 405 | wait=self.wait, payload_length=payload_length, payload=self.payload) 406 | 407 | @classmethod 408 | def from_bytes(cls, m_bytes): 409 | request_id = cls._get_request_id_from_bytes(m_bytes) 410 | wait = cls._get_wait_from_bytes(m_bytes) 411 | payload = cls._get_payload_from_bytes( 412 | m_bytes, payload_length_start=cls._wait_end) 413 | return cls(request_id, wait, payload) 414 | 415 | @classmethod 416 | def _get_wait_from_bytes(cls, m_bytes): 417 | return int(m_bytes[cls._wait_start: cls._wait_end], 16) 418 | 419 | 420 | class NotificationMessage(GotalkMessage): 421 | """ 422 | Notification = "n" name payload 423 | 424 | name = text3 425 | payload = payloadSize payloadData? 426 | payloadSize = hexUInt8 427 | payloadData = {payloadSize} 428 | 429 | text3 = text3Size text3Value 430 | text3Size = hexUInt3 431 | text3Value = <{text3Size} as utf8 text> 432 | 433 | +---------------------- Notification 434 | | +--------------------- text3Size 12 435 | | | +--------- name "chat message" 436 | | | | +- payloadSize 50 437 | | | | | 438 | n00cchat message00000032{"message":"Hi","from":"nthn","chat_room":"gonuts"} 439 | """ 440 | 441 | type_id = NOTIFICATION_TYPE 442 | 443 | def __init__(self, name, payload): 444 | self.name = name 445 | self.payload = payload 446 | 447 | def to_bytes(self): 448 | name_size = len(self.name) 449 | payload_length = self._check_payload_length(self.payload) 450 | return "{type_id}{name_size:03x}{name}{payload_length:08x}{payload}".format( 451 | type_id=self.type_id, name_size=name_size, name=self.name, 452 | payload_length=payload_length, payload=self.payload) 453 | 454 | @classmethod 455 | def from_bytes(cls, m_bytes): 456 | name, name_end = cls._get_name_from_bytes(m_bytes) 457 | payload = cls._get_payload_from_bytes( 458 | m_bytes, payload_length_start=name_end) 459 | return cls(name, payload) 460 | 461 | @classmethod 462 | def _get_name_from_bytes(cls, m_bytes): 463 | name_length = int(m_bytes[1:4], 16) 464 | name_end = 4 + name_length 465 | name = m_bytes[4: name_end] 466 | return name, name_end 467 | -------------------------------------------------------------------------------- /requirements/install.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtaylor/python-gotalk/7c26eaa514605490ad2b254a8fb2b0e05d652d45/requirements/install.txt -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | nose 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import re 4 | import sys 5 | 6 | from setuptools import setup, find_packages 7 | 8 | 9 | version = re.compile(r'VERSION\s*=\s*\((.*?)\)') 10 | 11 | 12 | def get_package_version(): 13 | """ 14 | :returns: package version without importing it. 15 | """ 16 | 17 | base = os.path.abspath(os.path.dirname(__file__)) 18 | with open(os.path.join(base, "gotalk/__init__.py")) as initf: 19 | for line in initf: 20 | m = version.match(line.strip()) 21 | if not m: 22 | continue 23 | return ".".join(m.groups()[0].split(", ")) 24 | 25 | 26 | def get_requirements(filename): 27 | return open('requirements/' + filename).read().splitlines() 28 | 29 | 30 | classes = """ 31 | Development Status :: 4 - Beta 32 | Intended Audience :: Developers 33 | License :: OSI Approved :: BSD License 34 | Topic :: System :: Distributed Computing 35 | Programming Language :: Python 36 | Programming Language :: Python :: 2 37 | Programming Language :: Python :: 2.6 38 | Programming Language :: Python :: 2.7 39 | Programming Language :: Python :: 3 40 | Programming Language :: Python :: 3.3 41 | Programming Language :: Python :: 3.4 42 | Programming Language :: Python :: Implementation :: CPython 43 | Operating System :: OS Independent 44 | """ 45 | classifiers = [s.strip() for s in classes.split('\n') if s] 46 | 47 | 48 | install_requires = get_requirements('install.txt') 49 | if sys.version_info < (3, 0): 50 | install_requires.append('futures') 51 | 52 | 53 | setup( 54 | name='gotalk', 55 | version=get_package_version(), 56 | description='A Python implementation of the Gotalk protocol.', 57 | long_description=open('README.rst').read(), 58 | author='Greg Taylor', 59 | author_email='gtaylor@gc-taylor.com', 60 | url='https://github.com/gtaylor/python-gotalk', 61 | license='BSD', 62 | classifiers=classifiers, 63 | packages=find_packages(exclude=['tests', 'tests.*']), 64 | install_requires=install_requires, 65 | test_suite="tests", 66 | tests_require=get_requirements('test.txt'), 67 | ) 68 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'gtaylor' 2 | -------------------------------------------------------------------------------- /tests/test_version01.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from gotalk.exceptions import PayloadTooLongError, OperationTooLongError 3 | 4 | from gotalk.protocol.messages import read_version_message, write_message, \ 5 | read_message 6 | from gotalk.protocol.version01.messages import ProtocolVersionMessage, \ 7 | SingleRequestMessage, SingleResultMessage, StreamRequestMessage, \ 8 | StreamRequestPartMessage, StreamResultMessage, ErrorResultMessage, \ 9 | NotificationMessage, RetryResultMessage, ProtocolErrorMessage 10 | 11 | 12 | _PROTO_VERSION = "01" 13 | 14 | 15 | class VersionTest(TestCase): 16 | 17 | def test_read_valid(self): 18 | """ 19 | Test parsing of valid version message. 20 | """ 21 | 22 | valid = _PROTO_VERSION 23 | self.assertEqual(read_version_message(valid), valid) 24 | 25 | def test_write_version(self): 26 | """ 27 | Test the writing of a version message. 28 | """ 29 | 30 | message = ProtocolVersionMessage() 31 | m_bytes = write_message(message) 32 | self.assertEqual(m_bytes, _PROTO_VERSION) 33 | 34 | 35 | class CommonTest(TestCase): 36 | """ 37 | The tests in this case apply to almost every message type. 38 | """ 39 | 40 | def test_large_payload(self): 41 | """ 42 | Make sure payload length errors are triggering. 43 | """ 44 | 45 | self.skipTest("See if there's a way to do this without the RAM usage.") 46 | valid_payload = "#" * SingleRequestMessage.payload_max_length 47 | message = SingleRequestMessage( 48 | request_id="0001", operation="echo", payload=valid_payload) 49 | # This shouldn't raise an error. 50 | write_message(message) 51 | # Now make it too big. 52 | message.payload += "#" 53 | self.assertRaises(PayloadTooLongError, write_message, message) 54 | 55 | def test_large_operation(self): 56 | """ 57 | Make sure operation length errors are triggering. 58 | """ 59 | 60 | valid_operation = "#" * SingleRequestMessage.operation_max_length 61 | message = SingleRequestMessage( 62 | request_id="0001", operation=valid_operation, payload="yay") 63 | # This shouldn't raise an error. 64 | write_message(message) 65 | # Now make it too big (by one). 66 | message.operation += "#" 67 | self.assertRaises(OperationTooLongError, write_message, message) 68 | 69 | 70 | class ProtocolErrorMessageTest(TestCase): 71 | 72 | def test_valid_read(self): 73 | """ 74 | Tests the reading of properly formed single request messages. 75 | """ 76 | 77 | valid1 = 'f00000001' 78 | message = read_message(valid1, _PROTO_VERSION) 79 | self.assertIsInstance(message, ProtocolErrorMessage) 80 | self.assertEqual(message.code, 1) 81 | 82 | def test_write(self): 83 | """ 84 | Makes sure our single request serialization is good. 85 | """ 86 | 87 | message = ProtocolErrorMessage(code=1) 88 | m_bytes = write_message(message) 89 | self.assertEqual(m_bytes, 'f00000001') 90 | 91 | 92 | class SingleRequestMessageTest(TestCase): 93 | 94 | def test_valid_read(self): 95 | """ 96 | Tests the reading of properly formed single request messages. 97 | """ 98 | 99 | valid1 = 'r0001004echo00000019{"message":"Hello World"}' 100 | message = read_message(valid1, _PROTO_VERSION) 101 | self.assertIsInstance(message, SingleRequestMessage) 102 | self.assertEqual(message.request_id, "0001") 103 | self.assertEqual(message.operation, "echo") 104 | self.assertEqual(message.payload, '{"message":"Hello World"}') 105 | 106 | def test_write(self): 107 | """ 108 | Makes sure our single request serialization is good. 109 | """ 110 | 111 | message = SingleRequestMessage( 112 | request_id="0001", operation="echo", payload="Hello World") 113 | m_bytes = write_message(message) 114 | self.assertEqual(m_bytes, 'r0001004echo0000000bHello World') 115 | 116 | 117 | class SingleResultMessageTest(TestCase): 118 | 119 | def test_valid_read(self): 120 | """ 121 | Tests the reading of properly formed single result messages. 122 | """ 123 | 124 | valid1 = 'R000100000019{"message":"Hello World"}' 125 | message = read_message(valid1, _PROTO_VERSION) 126 | self.assertIsInstance(message, SingleResultMessage) 127 | self.assertEqual(message.request_id, "0001") 128 | self.assertEqual(message.payload, '{"message":"Hello World"}') 129 | 130 | def test_write(self): 131 | """ 132 | Makes sure our single result serialization is good. 133 | """ 134 | 135 | message = SingleResultMessage( 136 | request_id="0001", payload="Hello World") 137 | m_bytes = write_message(message) 138 | self.assertEqual(m_bytes, 'R00010000000bHello World') 139 | 140 | 141 | class StreamRequestMessageTest(TestCase): 142 | 143 | def test_valid_read(self): 144 | """ 145 | Tests the reading of properly formed stream request messages. 146 | """ 147 | 148 | valid1 = 's0001004echo0000000b{"message":' 149 | message = read_message(valid1, _PROTO_VERSION) 150 | self.assertIsInstance(message, StreamRequestMessage) 151 | self.assertEqual(message.request_id, "0001") 152 | self.assertEqual(message.operation, "echo") 153 | self.assertEqual(message.payload, '{"message":') 154 | 155 | def test_write(self): 156 | """ 157 | Makes sure our stream request serialization is good. 158 | """ 159 | 160 | message = StreamRequestMessage( 161 | request_id="0001", operation="echo", payload="Hello World") 162 | m_bytes = write_message(message) 163 | self.assertEqual(m_bytes, 's0001004echo0000000bHello World') 164 | 165 | 166 | class StreamRequestPartMessageTest(TestCase): 167 | 168 | def test_valid_read(self): 169 | """ 170 | Tests the reading of properly formed stream request part messages. 171 | """ 172 | 173 | valid1 = 'p00010000000e"Hello World"}' 174 | message = read_message(valid1, _PROTO_VERSION) 175 | self.assertIsInstance(message, StreamRequestPartMessage) 176 | self.assertEqual(message.request_id, "0001") 177 | self.assertEqual(message.payload, '"Hello World"}') 178 | 179 | def test_write(self): 180 | """ 181 | Makes sure our stream request serialization is good. 182 | """ 183 | 184 | message = StreamRequestPartMessage( 185 | request_id="0001", payload="Hello World") 186 | m_bytes = write_message(message) 187 | self.assertEqual(m_bytes, 'p00010000000bHello World') 188 | 189 | 190 | class StreamResultMessageTest(TestCase): 191 | 192 | def test_valid_read(self): 193 | """ 194 | Tests the reading of properly formed stream result messages. 195 | """ 196 | 197 | valid1 = 'S00010000000b{"message":' 198 | message = read_message(valid1, _PROTO_VERSION) 199 | self.assertIsInstance(message, StreamResultMessage) 200 | self.assertEqual(message.request_id, "0001") 201 | self.assertEqual(message.payload, '{"message":') 202 | 203 | def test_write(self): 204 | """ 205 | Makes sure our stream result serialization is good. 206 | """ 207 | 208 | message = StreamResultMessage( 209 | request_id="0001", payload="Hello World") 210 | m_bytes = write_message(message) 211 | self.assertEqual(m_bytes, 'S00010000000bHello World') 212 | 213 | 214 | class ErrorResultMessageTest(TestCase): 215 | 216 | def test_valid_read(self): 217 | """ 218 | Tests the reading of properly formed error result messages. 219 | """ 220 | 221 | valid1 = 'E000100000026{"error":"Unknown operation \"echo\""}' 222 | message = read_message(valid1, _PROTO_VERSION) 223 | self.assertIsInstance(message, ErrorResultMessage) 224 | self.assertEqual(message.request_id, "0001") 225 | self.assertEqual(message.payload, '{"error":"Unknown operation \"echo\""}') 226 | 227 | def test_write(self): 228 | """ 229 | Makes sure our error result serialization is good. 230 | """ 231 | 232 | message = ErrorResultMessage( 233 | request_id="0001", payload="Hello World") 234 | m_bytes = write_message(message) 235 | self.assertEqual(m_bytes, 'E00010000000bHello World') 236 | 237 | 238 | class RetryResultMessageTest(TestCase): 239 | 240 | def test_valid_read(self): 241 | """ 242 | Tests the reading of properly formed retry result messages. 243 | """ 244 | 245 | valid1 = 'e00010000000000000014"service restarting"' 246 | message = read_message(valid1, _PROTO_VERSION) 247 | self.assertIsInstance(message, RetryResultMessage) 248 | self.assertEqual(message.request_id, "0001") 249 | self.assertEqual(message.payload, '"service restarting"') 250 | 251 | def test_write(self): 252 | """ 253 | Makes sure our error result serialization is good. 254 | """ 255 | 256 | message = RetryResultMessage( 257 | request_id="0001", wait=0, payload='"service restarting"') 258 | m_bytes = write_message(message) 259 | self.assertEqual(m_bytes, 'e00010000000000000014"service restarting"') 260 | 261 | 262 | class NotificationMessageTest(TestCase): 263 | 264 | def test_valid_read(self): 265 | """ 266 | Tests the reading of properly formed notification messages. 267 | """ 268 | 269 | valid1 = 'n00cchat message00000032{"message":"Hi","from":"nthn","chat_room":"gonuts"}' 270 | message = read_message(valid1, _PROTO_VERSION) 271 | self.assertIsInstance(message, NotificationMessage) 272 | self.assertEqual(message.name, "chat message") 273 | self.assertEqual(message.payload, '{"message":"Hi","from":"nthn","chat_room":"gonuts"}') 274 | 275 | def test_write(self): 276 | """ 277 | Makes sure our notification serialization is good. 278 | """ 279 | 280 | message = NotificationMessage(name="test_name", payload="Hello World") 281 | m_bytes = write_message(message) 282 | self.assertEqual(m_bytes, 'n009test_name0000000bHello World') 283 | --------------------------------------------------------------------------------