gntplib 0.5__py3-none-any.whl → 0.66__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.

Potentially problematic release.


This version of gntplib might be problematic. Click here for more details.

gntplib/__init__.py CHANGED
@@ -1,959 +1,973 @@
1
- """This module provides core functionalities of gntplib.
2
-
3
- gntplib is a Growl Notification Transport Protocol (GNTP_) client library in
4
- Python.
5
-
6
- .. _GNTP: http://www.growlforwindows.com/gfw/help/gntp.aspx
7
- """
8
-
9
- from __future__ import unicode_literals
10
- import hashlib
11
- import io
12
- import re
13
- import socket
14
-
15
- from . import keys
16
- from .compat import text_type
17
- from .exceptions import GNTPError
18
-
19
-
20
- __version__ = '0.5'
21
- __all__ = ['notify', 'publish', 'subscribe', 'Event', 'Notifier', 'Publisher',
22
- 'RawIcon', 'Resource', 'SocketCallback', 'Subscriber']
23
-
24
-
25
- SUPPORTED_VERSIONS = ['1.0']
26
- DEFAULT_PORT = 23053
27
- DEFAULT_TIMEOUT = 10
28
- DEFAULT_TTL = 60
29
- MAX_MESSAGE_SIZE = 4096
30
- MAX_LINE_SIZE = 1024
31
- LINE_DELIMITER = b'\r\n'
32
- SECTION_DELIMITER = SECTION_BODY_START = SECTION_BODY_END = b'\r\n'
33
- MESSAGE_DELIMITER = b'\r\n\r\n'
34
- MESSAGE_DELIMITER_SIZE = len(MESSAGE_DELIMITER)
35
- RESPONSE_INFORMATION_LINE_RE = re.compile(
36
- b'GNTP/([^ ]+) (-OK|-ERROR|-CALLBACK) NONE')
37
- CUSTOM_HEADER_PREFIX = 'X-'
38
- APP_SPECIFIC_HEADER_PREFIX = 'Data-'
39
-
40
-
41
- def publish(app_name, event_name, title, text=''):
42
- """Register a publisher and send a notification at a time.
43
-
44
- :param app_name: the name of the application.
45
- :param event_name: the name of the notification.
46
- :param title: the title of the notification.
47
- :param text: the text of the notification. Defaults to ``''``.
48
- """
49
- publisher = Publisher(app_name, coerce_to_events([event_name]))
50
- publisher.register()
51
- publisher.publish(event_name, title, text)
52
-
53
-
54
- def notify(app_name, event_name, title, text=''):
55
- """Deprecated notify function."""
56
- import warnings
57
- warnings.warn('notify function is deprecated, use publish function'
58
- ' instead', DeprecationWarning, stacklevel=2)
59
- publish(app_name, event_name, title, text)
60
-
61
-
62
- def subscribe(id_, name, hub, password, port=DEFAULT_PORT):
63
- """Send a subscription request and return ttl.
64
-
65
- :param id_: the unique id of the subscriber.
66
- :param name: the name of the subscriber.
67
- :param hub: the subscribed-to machine. If a string is given, it is used as
68
- a host of the hub and default port number `23053` is used.
69
- If host-port tuple is given, it is used directly.
70
- :param password: the password of the subscribed-to machine.
71
- :param port: the port number of the subscriber. Defaults to `23053`.
72
- """
73
- subscriber = Subscriber(id_, name, hub, password, port=port)
74
- subscriber.subscribe()
75
- return subscriber.ttl
76
-
77
-
78
- class BaseApp(object):
79
- """Base class for applications.
80
-
81
- :param custom_headers: the list of key-value tuples for Custom Headers.
82
- :param app_specific_headers: the list of key-value tuples for App-Specific
83
- Headers.
84
- :param gntp_client_class: GNTP client class. If it is `None`,
85
- :class:`GNTPClient` is used. Defaults to `None`.
86
- """
87
-
88
- def __init__(self, custom_headers=None, app_specific_headers=None,
89
- gntp_client_class=None, **kwargs):
90
- self.custom_headers = custom_headers or []
91
- self.app_specific_headers = app_specific_headers or []
92
- if gntp_client_class is None:
93
- gntp_client_class = GNTPClient
94
- self.gntp_client = gntp_client_class(**kwargs)
95
-
96
-
97
- class Publisher(BaseApp):
98
- """Publisher of Growl Notification Transport Protocol (GNTP).
99
-
100
- This class supports ``REGISTER`` and ``NOTIFY`` requests. They
101
- are sent by :meth:`register()` and :meth:`publish()` methods respectively.
102
- These methods can accept the optional final callback as `callback` keyword
103
- argument, which run after closing the connection with the GNTP server.
104
-
105
- `event_defs` is a list of ``str``, ``unicode``, double (of ``str`` and
106
- ``bool``) or :class:`Event` instance. It is converted to a list of
107
- :class:`Event` instance as follows:
108
- ``str`` or ``unicode`` item becomes value of the `name` attribute of
109
- :class:`Event` instance, whose other attributes are defaults. Double item
110
- is expanded to (`name`, `enabled`) tuple, and those values are passed to
111
- :class:`Event` constructor. :class:`Event` instance item is used directly.
112
-
113
- Optional keyword arguments are passed to the `gntp_client_class`
114
- constructor.
115
-
116
- :param name: the name of the application.
117
- :param event_defs: the definitions of the notifications.
118
- :param icon: url string or an instance of :class:`Resource` for the icon of
119
- the application. Defaults to `None`.
120
- :param custom_headers: the list of key-value tuples for Custom Headers.
121
- :param app_specific_headers: the list of key-value tuples for App-Specific
122
- Headers.
123
- :param gntp_client_class: GNTP client class. If it is `None`,
124
- :class:`GNTPClient` is used. Defaults to `None`.
125
-
126
- .. note:: In Growl 1.3.3, `icon` of url string does not work.
127
- """
128
-
129
- def __init__(self, name, event_defs, icon=None, custom_headers=None,
130
- app_specific_headers=None, gntp_client_class=None, **kwargs):
131
- self.name = name
132
- self.icon = icon
133
- self.events = coerce_to_events(event_defs)
134
- if not self.events:
135
- raise GNTPError('You have to set at least one notification type')
136
- BaseApp.__init__(self, custom_headers, app_specific_headers,
137
- gntp_client_class, **kwargs)
138
-
139
- def register(self, callback=None):
140
- """Register this publisher to the GNTP server.
141
-
142
- :param callback: the callback run after closing the connection with
143
- the GNTP server. Defaults to `None`.
144
- """
145
- request = RegisterRequest(self.name, self.icon, self.events,
146
- self.custom_headers,
147
- self.app_specific_headers)
148
- self.gntp_client.process_request(request, callback)
149
-
150
- def publish(self, name, title, text='', id_=None, sticky=False,
151
- priority=0, icon=None, coalescing_id=None, callback=None,
152
- gntp_callback=None, **socket_callback_options):
153
- """Send a notification to the GNTP server.
154
-
155
- :param name: the name of the notification.
156
- :param title: the title of the notification.
157
- :param text: the text of the notification. Defaults to `''`.
158
- :param id_: the unique ID for the notification. If set, this should be
159
- unique for every request. Defaults to `None`.
160
- :param sticky: if set to `True`, the notification remains displayed
161
- until dismissed by the user. Defaults to `False`.
162
- :param priority: the display hint for the receiver which may be
163
- ignored. A higher number indicates a higher priority.
164
- Valid values are between -2 and 2, defaults to `0`.
165
- :param icon: url string or an instance of :class:`Resource` to display
166
- with the notification. Defaults to `None`.
167
- :param coalescing_id: if set, should contain the value of the `id_` of
168
- a previously-sent notification.
169
- This serves as a hint to the notification system
170
- that this notification should replace/update the
171
- matching previous notification. The notification
172
- system may ignore this hint. Defaults to `None`.
173
- :param callback: the callback run after closing the connection with
174
- the GNTP server. Defaults to `None`.
175
- :param gntp_callback: url string for url callback or
176
- :class:`SocketCallback` instance for socket
177
- callback. Defaults to `None`.
178
- :param socket_callback_options: the keyword arguments to be used to
179
- instantiating :class:`SocketCallback`
180
- for socket callback. About acceptable
181
- keyword arguments,
182
- see :class:`SocketCallback`.
183
-
184
- .. note:: In Growl 1.3.3, `icon` of url string does not work.
185
-
186
- .. note:: Growl for Windows v2.0+ and Growl v1.3+ require
187
- `coalescing_id` to be the same on both the original and
188
- updated notifcation, ignoring the value of `id_`.
189
- """
190
- notification = Notification(name, title, text, id_=id_, sticky=sticky,
191
- priority=priority, icon=icon,
192
- coalescing_id=coalescing_id,
193
- gntp_callback=gntp_callback,
194
- **socket_callback_options)
195
- request = NotifyRequest(self.name, notification,
196
- self.custom_headers,
197
- self.app_specific_headers)
198
- self.gntp_client.process_request(
199
- request, callback, socket_callback=notification.socket_callback)
200
-
201
-
202
- class Notifier(Publisher):
203
- """Deprecated Notifier of Growl Notification Transport Protocol (GNTP)."""
204
-
205
- def __init__(self, name, event_defs, icon=None, custom_headers=None,
206
- app_specific_headers=None, gntp_client_class=None, **kwargs):
207
- import warnings
208
- warnings.warn('Notifier is deprecated, use Publisher instead',
209
- DeprecationWarning, stacklevel=2)
210
- Publisher.__init__(self, name, event_defs, icon,
211
- custom_headers, app_specific_headers,
212
- gntp_client_class, **kwargs)
213
-
214
- def notify(self, name, title, text='', id_=None, sticky=False,
215
- priority=0, icon=None, coalescing_id=None, callback=None,
216
- gntp_callback=None, **socket_callback_options):
217
- """Send a notification to the GNTP server."""
218
- import warnings
219
- warnings.warn('notify method is deprecated, use publish method'
220
- ' instead', DeprecationWarning, stacklevel=2)
221
- self.publish(name, title, text, id_, sticky, priority, icon,
222
- coalescing_id, callback, gntp_callback,
223
- **socket_callback_options)
224
-
225
-
226
- class Subscriber(BaseApp):
227
- """Subscriber of Growl Notification Transport Protocol (GNTP).
228
-
229
- This class supports ``SUBSCRIBE`` request.
230
-
231
- :param id_: the unique id of the subscriber.
232
- :param name: the name of the subscriber.
233
- :param hub: the subscribed-to machine. If a string is given, it is used as
234
- a host of the hub and default port number `23053` is used.
235
- If host-port tuple is given, it is used directly.
236
- :param password: the password of the subscribed-to machine.
237
- :param port: the port number of the subscriber. Defaults to `23053`.
238
- :param custom_headers: the list of key-value tuples for Custom Headers.
239
- :param app_specific_headers: the list of key-value tuples for App-Specific
240
- Headers.
241
- :param gntp_client_class: GNTP client class. If it is `None`,
242
- :class:`GNTPClient` is used. Defaults to `None`.
243
- """
244
-
245
- def __init__(self, id_, name, hub, password, port=DEFAULT_PORT,
246
- custom_headers=None, app_specific_headers=None,
247
- gntp_client_class=None, **kwargs):
248
- self.id_ = id_
249
- self.name = name
250
- if isinstance(hub, (bytes, text_type)):
251
- self.hub = (hub, DEFAULT_PORT)
252
- else:
253
- self.hub = hub
254
- self.password = password
255
- self.port = port
256
- self.ttl = DEFAULT_TTL
257
- BaseApp.__init__(self, custom_headers, app_specific_headers,
258
- gntp_client_class, host=self.hub[0], port=self.hub[1],
259
- password=self.password, **kwargs)
260
-
261
- def subscribe(self, callback=None):
262
- """Send a subscription request.
263
-
264
- If `callback` is `None`, :meth:`store_ttl` is used and :attr:`ttl` is
265
- updated by ``Subscription-TTL`` value of the response.
266
-
267
- :param callback: the callback run after closing the connection with
268
- the GNTP server. Defaults to `None`.
269
- """
270
- request = SubscribeRequest(self.id_, self.name, self.port,
271
- self.custom_headers,
272
- self.app_specific_headers)
273
- self.gntp_client.process_request(request, callback or self.store_ttl)
274
-
275
- def store_ttl(self, response):
276
- """Update :attr:`ttl` attribute."""
277
- ttl = response.headers['Subscription-TTL']
278
- self.ttl = int(ttl)
279
-
280
-
281
- class Event(object):
282
- """Represent notification type.
283
-
284
- :param name: the name of the notification.
285
- :param display_name: the display name of the notification, which is
286
- appeared at the Applications tab of the Growl
287
- Preferences. Defaults to `None`.
288
- :param enabled: indicates if the notification should be enabled by
289
- default. Defaults to `True`.
290
- :param icon: url string or an instance of :class:`Resource` for the default
291
- icon to display with the notifications of this notification
292
- type. Defaults to `None`.
293
-
294
- .. note:: In Growl 1.3.3, `icon` does not work.
295
- """
296
-
297
- def __init__(self, name, display_name=None, enabled=True, icon=None):
298
- self.name = name
299
- self.display_name = display_name
300
- self.enabled = enabled
301
- self.icon = icon
302
-
303
-
304
- class Notification(object):
305
- """Represent notification."""
306
-
307
- def __init__(self, name, title, text='', id_=None, sticky=None,
308
- priority=None, icon=None, coalescing_id=None,
309
- gntp_callback=None, **socket_callback_options):
310
- self.name = name
311
- self.title = title
312
- self.text = text
313
- self.id_ = id_
314
- self.sticky = sticky
315
- self.priority = priority
316
- self.icon = icon
317
- self.coalescing_id = coalescing_id
318
- self.callback = coerce_to_callback(gntp_callback,
319
- **socket_callback_options)
320
-
321
- @property
322
- def socket_callback(self):
323
- if isinstance(self.callback, SocketCallback):
324
- return self.callback
325
-
326
-
327
- class BaseRequest(object):
328
- """Abstract base class for GNTP request.
329
-
330
- :param custom_headers: the list of key-value tuples for Custom Headers.
331
- :param app_specific_headers: the list of key-value tuples for App-Specific
332
- Headers.
333
- """
334
- #: Request message type. Subclasses must override this attribute.
335
- message_type = None
336
-
337
- def __init__(self, custom_headers=None, app_specific_headers=None):
338
- self.custom_headers = custom_headers or []
339
- self.app_specific_headers = app_specific_headers or []
340
-
341
- def write_into(self, writer):
342
- """Subclasses must call this method first to serialize their
343
- message."""
344
- writer.write_base_request(self)
345
-
346
-
347
- class RegisterRequest(BaseRequest):
348
- """Represent ``REGISTER`` request.
349
-
350
- :param app_name: the name of the application.
351
- :param app_icon: url string or an instance of :class:`Resource` for the
352
- icon of the application.
353
- :param events: the list of :class:`Event` instances.
354
- :param custom_headers: the list of key-value tuples for Custom Headers.
355
- :param app_specific_headers: the list of key-value tuples for App-Specific
356
- Headers.
357
-
358
- .. note:: In Growl 1.3.3, `app_icon` of url string does not work.
359
- """
360
- message_type = 'REGISTER'
361
-
362
- def __init__(self, app_name, app_icon, events, custom_headers=None,
363
- app_specific_headers=None):
364
- BaseRequest.__init__(self, custom_headers, app_specific_headers)
365
- self.app_name = app_name
366
- self.app_icon = app_icon
367
- self.events = events
368
-
369
- def write_into(self, writer):
370
- BaseRequest.write_into(self, writer)
371
- writer.write_register_request(self)
372
-
373
-
374
- class NotifyRequest(BaseRequest):
375
- """Represent ``NOTIFY`` request.
376
-
377
- :param app_name: the name of the application.
378
- :param notification: :class:`Notification` instance.
379
- :param custom_headers: the list of key-value tuples for Custom Headers.
380
- :param app_specific_headers: the list of key-value tuples for App-Specific
381
- Headers.
382
- """
383
- message_type = 'NOTIFY'
384
-
385
- def __init__(self, app_name, notification, custom_headers=None,
386
- app_specific_headers=None):
387
- BaseRequest.__init__(self, custom_headers, app_specific_headers)
388
- self.app_name = app_name
389
- self.notification = notification
390
-
391
- def write_into(self, writer):
392
- BaseRequest.write_into(self, writer)
393
- writer.write_notify_request(self)
394
-
395
-
396
- class SubscribeRequest(BaseRequest):
397
- """Represent ``SUBSCRIBE`` request.
398
-
399
- :param id_: the unique id of the subscriber.
400
- :param name: the name of the subscriber.
401
- :param port: the port number of the subscriber.
402
- :param custom_headers: the list of key-value tuples for Custom Headers.
403
- :param app_specific_headers: the list of key-value tuples for App-Specific
404
- Headers.
405
- """
406
- message_type = 'SUBSCRIBE'
407
-
408
- def __init__(self, id_, name, port, custom_headers=None,
409
- app_specific_headers=None):
410
- BaseRequest.__init__(self, custom_headers, app_specific_headers)
411
- self.id_ = id_
412
- self.name = name
413
- self.port = port
414
-
415
- def write_into(self, writer):
416
- BaseRequest.write_into(self, writer)
417
- writer.write_subscribe_request(self)
418
-
419
-
420
- class Response(object):
421
- """Base class for GNTP response.
422
-
423
- :param message_type: <messagetype> of the response. `'-OK'`, `'-ERROR'` or
424
- `'-CALLBACK'`.
425
- :param headers: headers of the response.
426
- """
427
-
428
- def __init__(self, message_type, headers):
429
- self.message_type = message_type
430
- self.headers = headers
431
-
432
-
433
- class BaseGNTPConnection(object):
434
- """Abstract base class for GNTP connection."""
435
-
436
- def __init__(self, final_callback, socket_callback=None):
437
- self.final_callback = final_callback
438
- self.socket_callback = socket_callback
439
-
440
- def on_ok_message(self, message):
441
- r"""Callback for ``-OK`` response.
442
-
443
- :param message: string of response terminated by `'\\r\\n\\r\\n'`.
444
- """
445
- try:
446
- response = parse_response(message, '-OK')
447
- if self.socket_callback is not None:
448
- self.read_message(self.on_callback_message)
449
- finally:
450
- if self.socket_callback is None:
451
- self.close()
452
- if self.socket_callback is None and self.final_callback is not None:
453
- self.final_callback(response)
454
-
455
- def on_callback_message(self, message):
456
- r"""Callback for ``-CALLBACK`` response.
457
-
458
- :param message: string of response terminated by `'\\r\\n\\r\\n'`.
459
- """
460
- try:
461
- response = parse_response(message, '-CALLBACK')
462
- callback_result = self.socket_callback(response)
463
- finally:
464
- self.close()
465
- if self.final_callback is not None:
466
- self.final_callback(callback_result)
467
-
468
- def write_message(self, message):
469
- """Subclasses must override this method to send a message to the GNTP
470
- server."""
471
- raise NotImplementedError
472
-
473
- def read_message(self, callback):
474
- """Subclasses must override this method to receive a message from the
475
- GNTP server."""
476
- raise NotImplementedError
477
-
478
- def close(self):
479
- """Subclasses must override this method to close the connection with
480
- the GNTP server."""
481
- raise NotImplementedError
482
-
483
-
484
- class GNTPConnection(BaseGNTPConnection):
485
- """Represent the connection with the GNTP server."""
486
-
487
- def __init__(self, address, timeout, final_callback, socket_callback=None):
488
- BaseGNTPConnection.__init__(self, final_callback, socket_callback)
489
- self.sock = socket.create_connection(address, timeout=timeout)
490
-
491
- def write_message(self, message):
492
- """Send the request message to the GNTP server."""
493
- self.sock.send(message)
494
-
495
- def read_message(self, callback):
496
- """Read a message from opened socket and run callback with it."""
497
- message = next(generate_messages(self.sock))
498
- callback(message)
499
-
500
- def close(self):
501
- """Close the socket."""
502
- self.sock.close()
503
- self.sock = None
504
-
505
-
506
- class GNTPClient(object):
507
- """GNTP client.
508
-
509
- :param host: host of GNTP server. Defaults to `'localhost'`.
510
- :param port: port of GNTP server. Defaults to `23053`.
511
- :param timeout: timeout in seconds. Defaults to `10`.
512
- :param password: the password used in creating the key.
513
- :param key_hashing: the type of hash algorithm used in creating the key.
514
- It is `keys.MD5`, `keys.SHA1`, `keys.SHA256` or
515
- `keys.SHA512`. Defaults to `keys.SHA256`.
516
- :param encryption: the tyep of encryption algorithm used.
517
- It is `None`, `ciphers.AES`, `ciphers.DES` or
518
- `ciphers.3DES`. `None` means no encryption.
519
- Defaults to `None`.
520
- :param connection_class: GNTP connection class. If it is `None`,
521
- :class:`GNTPConnection` is used. Defaults to
522
- `None`.
523
- """
524
-
525
- def __init__(self, host='localhost', port=DEFAULT_PORT,
526
- timeout=DEFAULT_TIMEOUT, password=None,
527
- key_hashing=keys.SHA256, encryption=None,
528
- connection_class=None):
529
- self.address = (host, port)
530
- self.timeout = timeout
531
- self.connection_class = connection_class or GNTPConnection
532
- if (encryption is not None and
533
- encryption.key_size > key_hashing.key_size):
534
- raise GNTPError('key_hashing key size (%s:%d) must be at'
535
- ' least encryption key size (%s:%d)' % (
536
- key_hashing.algorithm_id, key_hashing.key_size,
537
- encryption.algorithm_id, encryption.key_size))
538
- self.packer_factory = MessagePackerFactory(password, key_hashing,
539
- encryption)
540
-
541
- def process_request(self, request, callback, **kwargs):
542
- """Process a request.
543
-
544
- :param callback: the final callback run after closing connection.
545
- """
546
- packer = self.packer_factory.create()
547
- message = packer.pack(request)
548
- conn = self._connect(callback, **kwargs)
549
- conn.write_message(message)
550
- conn.read_message(conn.on_ok_message)
551
-
552
- def _connect(self, final_callback, **kwargs):
553
- """Connect to the GNTP server and return the connection."""
554
- return self.connection_class(self.address, self.timeout,
555
- final_callback, **kwargs)
556
-
557
-
558
- def generate_messages(sock, size=1024):
559
- """Generate messages from opened socket."""
560
- buf = b''
561
- while True:
562
- buf += sock.recv(size)
563
- if not buf:
564
- break
565
- pos = buf.find(MESSAGE_DELIMITER)
566
- if ((pos < 0 and len(buf) >= MAX_MESSAGE_SIZE) or
567
- (pos > MAX_MESSAGE_SIZE - MESSAGE_DELIMITER_SIZE)):
568
- raise GNTPError('too large message: %r' % buf)
569
- elif pos > 0:
570
- pos += 4
571
- yield buf[:pos]
572
- buf = buf[pos:]
573
-
574
-
575
- def parse_response(message, expected_message_type=None):
576
- """Parse response and return response object."""
577
- try:
578
- lines = [line for line in message.split(LINE_DELIMITER) if line]
579
- _, message_type = parse_information_line(lines.pop(0))
580
- if (expected_message_type is not None and
581
- expected_message_type != message_type):
582
- raise GNTPError('%s is not expected message type %s' % (
583
- message_type, expected_message_type))
584
-
585
- headers = dict([s.strip().decode('utf-8') for s in line.split(b':', 1)]
586
- for line in lines)
587
- if message_type == '-ERROR':
588
- raise GNTPError('%s: %s' % (headers['Error-Code'],
589
- headers['Error-Description']))
590
- return Response(message_type, headers)
591
- except ValueError as exc:
592
- raise GNTPError(exc.args[0], 'original message: %r' % message)
593
- except GNTPError as exc:
594
- exc.args = (exc.args[0], 'original message: %r' % message)
595
- raise exc
596
-
597
-
598
- def parse_information_line(line):
599
- """Parse information line and return tuple (`<version>`,
600
- `<messagetype>`)."""
601
- matched = RESPONSE_INFORMATION_LINE_RE.match(line)
602
- if matched is None:
603
- raise GNTPError('invalid information line: %r' % line)
604
- version, message_type = [s.decode('utf-8') for s in matched.groups()]
605
- if version not in SUPPORTED_VERSIONS:
606
- raise GNTPError("version '%s' is not supported" % version)
607
- return version, message_type
608
-
609
-
610
- def coerce_to_events(items):
611
- """Coerce the list of the event definitions to the list of :class:`Event`
612
- instances."""
613
- results = []
614
- for item in items:
615
- if isinstance(item, (bytes, text_type)):
616
- results.append(Event(item, enabled=True))
617
- elif isinstance(item, tuple):
618
- name, enabled = item
619
- results.append(Event(name, enabled=enabled))
620
- elif isinstance(item, Event):
621
- results.append(item)
622
- return results
623
-
624
-
625
- class Resource(object):
626
- """Class for <uniqueid> data types.
627
-
628
- :param data: the binary content.
629
- """
630
-
631
- def __init__(self, data):
632
- self.data = data
633
- self._unique_value = None
634
-
635
- def unique_value(self):
636
- """Return the <uniquevalue> value."""
637
- if self.data is not None and self._unique_value is None:
638
- self._unique_value = \
639
- hashlib.md5(self.data).hexdigest().encode('utf-8')
640
- return self._unique_value
641
-
642
- def unique_id(self):
643
- """Return the <uniqueid> value."""
644
- if self.data is not None:
645
- return b'x-growl-resource://' + self.unique_value()
646
-
647
-
648
- class RawIcon(Resource):
649
- """Deprecated icon class."""
650
-
651
- def __init__(self, data):
652
- import warnings
653
- warnings.warn('RawIcon is deprecated, use Resource instead',
654
- DeprecationWarning, stacklevel=2)
655
- Resource.__init__(self, data)
656
-
657
-
658
- class SocketCallback(object):
659
- """Base class for socket callback.
660
-
661
- Each of the callbacks takes one positional argument, which is
662
- :class:`Response` instance.
663
-
664
- :param context: value of ``Notification-Callback-Context``.
665
- Defaults to ``'None'``.
666
- :param context-type: value of ``Notification-Callback-Context-Type``.
667
- Defaults to ``'None'``.
668
- :param on_click: the callback run at ``CLICKED`` callback result.
669
- :param on_close: the callback run at ``CLOSED`` callback result.
670
- :param on_timeout: the callback run at ``TIMEDOUT`` callback result.
671
-
672
- .. note:: TIMEDOUT callback does not occur in my Growl 1.3.3.
673
- """
674
-
675
- def __init__(self, context='None', context_type='None',
676
- on_click=None, on_close=None, on_timeout=None):
677
- self.context = context
678
- self.context_type = context_type
679
- self.on_click_callback = on_click
680
- self.on_close_callback = on_close
681
- self.on_timeout_callback = on_timeout
682
-
683
- def on_click(self, response):
684
- """Run ``CLICKED`` event callback."""
685
- if self.on_click_callback is not None:
686
- return self.on_click_callback(response)
687
-
688
- def on_close(self, response):
689
- """Run ``CLOSED`` event callback."""
690
- if self.on_close_callback is not None:
691
- return self.on_close_callback(response)
692
-
693
- def on_timeout(self, response):
694
- """Run ``TIMEDOUT`` event callback."""
695
- if self.on_timeout_callback is not None:
696
- return self.on_timeout_callback(response)
697
-
698
- def __call__(self, response):
699
- """This is the callback. Delegate to ``on_`` methods depending on
700
- ``Notification-Callback-Result`` value.
701
-
702
- :param response: :class:`Response` instance.
703
- """
704
- callback_result = response.headers['Notification-Callback-Result']
705
- delegate_map = {
706
- 'CLICKED': self.on_click, 'CLICK': self.on_click,
707
- 'CLOSED': self.on_close, 'CLOSE': self.on_close,
708
- 'TIMEDOUT': self.on_timeout, 'TIMEOUT': self.on_timeout,
709
- }
710
- return delegate_map[callback_result](response)
711
-
712
- def write_into(self, writer):
713
- writer.write_socket_callback(self)
714
-
715
-
716
- class URLCallback(object):
717
- """Class for url callback."""
718
-
719
- def __init__(self, url):
720
- self.url = url
721
-
722
- def write_into(self, writer):
723
- writer.write_url_callback(self)
724
-
725
-
726
- def coerce_to_callback(gntp_callback=None, **socket_callback_options):
727
- """Return :class:`URLCallback` instance for url callback or
728
- :class:`SocketCallback` instance for socket callback.
729
-
730
- If `gntp_callback` is not `None`, `socket_callback_options` must be empty.
731
- Moreover, if `gntp_callback` is string, then a instance of
732
- :class:`URLCallback` is returned. Otherwise, `gntp_callback` is returned
733
- directly.
734
-
735
- If `gntp_callback` is `None` and `socket_callback_options` is not empty,
736
- new instance of :class:`SocketCallback` is created from given keyword
737
- arguments and it is returned. Acceptable keyword arguments are same as
738
- constructor's of :class:`SocketCallback`.
739
- """
740
- if gntp_callback is not None:
741
- if socket_callback_options:
742
- raise GNTPError('If gntp_callback is not None,'
743
- ' socket_callback_options must be empty')
744
- if isinstance(gntp_callback, (bytes, text_type)):
745
- return URLCallback(gntp_callback)
746
- else:
747
- return gntp_callback
748
- if socket_callback_options:
749
- return SocketCallback(**socket_callback_options)
750
-
751
-
752
- class _NullCipher(object):
753
- """Null object for the encryption of messages."""
754
-
755
- algorithm = None
756
- algorithm_id = 'NONE'
757
- encrypt = lambda self, text: text
758
- decrypt = lambda self, text: text
759
- __bool__ = lambda self: False
760
- __nonzero__ = __bool__
761
-
762
-
763
- NullCipher = _NullCipher()
764
-
765
-
766
- class MessagePackerFactory(object):
767
- """The factory of :class:`MessagePacker`.
768
-
769
- If `password` is None, `hashing` and `encryption` are ignored.
770
- """
771
-
772
- def __init__(self, password=None, hashing=keys.SHA256, encryption=None):
773
- self.password = password
774
- self.hashing = password and hashing
775
- self.encryption = (password and encryption) or NullCipher
776
-
777
- def create(self):
778
- """Create an instance of :class:`MessagePacker` and return it."""
779
- key = self.password and self.hashing.key(self.password)
780
- cipher = self.encryption and self.encryption.cipher(key)
781
- return MessagePacker(key, cipher)
782
-
783
-
784
- class MessagePacker(object):
785
- """The serializer for messages.
786
-
787
- `key` and `cipher` have random-generated salt and iv respectively.
788
-
789
- :param key: an instance of :class:`keys.Key`.
790
- :param cipher: an instance of :class:`ciphers.Cipher` or `NullCipher`.
791
- """
792
-
793
- def __init__(self, key=None, cipher=None):
794
- self.key = key
795
- self.cipher = cipher or NullCipher
796
-
797
- def pack(self, request):
798
- """Return utf-8 encoded request message."""
799
- return (InformationLinePacker(self.key, self.cipher).pack(request) +
800
- LINE_DELIMITER +
801
- HeaderPacker(self.cipher).pack(request) +
802
- SectionPacker(self.cipher).pack(request) +
803
- LINE_DELIMITER)
804
-
805
-
806
- class InformationLinePacker(object):
807
-
808
- def __init__(self, key, cipher):
809
- self.key = key
810
- self.cipher = cipher
811
-
812
- def pack(self, request):
813
- """Return utf-8 encoded information line."""
814
- result = (b'GNTP/1.0 ' +
815
- request.message_type.encode('utf-8') +
816
- b' ' +
817
- self.cipher.algorithm_id.encode('utf-8'))
818
- if self.cipher.algorithm is not None:
819
- result += b':' + self.cipher.iv_hex
820
- if self.key is not None:
821
- result += (b' ' +
822
- self.key.algorithm_id.encode('utf-8') +
823
- b':' +
824
- self.key.key_hash_hex +
825
- b'.' +
826
- self.key.salt_hex)
827
- return result
828
-
829
-
830
- class HeaderPacker(object):
831
-
832
- def __init__(self, cipher):
833
- self.writer = io.BytesIO()
834
- self.cipher = cipher
835
-
836
- def pack(self, request):
837
- """Return utf-8 encoded headers."""
838
- request.write_into(self)
839
- headers = self.writer.getvalue()
840
- result = self.cipher.encrypt(headers)
841
- if self.cipher.algorithm is not None:
842
- result += LINE_DELIMITER
843
- return result
844
-
845
- def write_base_request(self, request):
846
- self._write_additional_headers(request.custom_headers,
847
- CUSTOM_HEADER_PREFIX)
848
- self._write_additional_headers(request.app_specific_headers,
849
- APP_SPECIFIC_HEADER_PREFIX)
850
-
851
- def _write_additional_headers(self, headers, prefix):
852
- for key, value in headers:
853
- if not key.startswith(prefix):
854
- key = prefix + key
855
- self.write(key.encode('utf-8'), value)
856
-
857
- def write_register_request(self, request):
858
- self.write(b'Application-Name', request.app_name)
859
- self.write(b'Application-Icon', request.app_icon)
860
- self.write(b'Notifications-Count', len(request.events))
861
- for event in request.events:
862
- self.writer.write(LINE_DELIMITER)
863
- self.write(b'Notification-Name', event.name)
864
- self.write(b'Notification-Display-Name', event.display_name)
865
- self.write(b'Notification-Enabled', event.enabled)
866
- self.write(b'Notification-Icon', event.icon)
867
-
868
- def write_notify_request(self, request):
869
- self.write(b'Application-Name', request.app_name)
870
- self._write_notification(request.notification)
871
-
872
- def write_subscribe_request(self, request):
873
- self.write(b'Subscriber-ID', request.id_)
874
- self.write(b'Subscriber-Name', request.name)
875
- self.write(b'Subscriber-Port', request.port)
876
-
877
- def _write_notification(self, notification):
878
- self.write(b'Notification-Name', notification.name)
879
- self.write(b'Notification-ID', notification.id_)
880
- self.write(b'Notification-Title', notification.title)
881
- self.write(b'Notification-Text', notification.text)
882
- self.write(b'Notification-Sticky', notification.sticky)
883
- self.write(b'Notification-Priority', notification.priority)
884
- self.write(b'Notification-Icon', notification.icon)
885
- self.write(b'Notification-Coalescing-ID', notification.coalescing_id)
886
- if notification.callback is not None:
887
- notification.callback.write_into(self)
888
-
889
- def write_socket_callback(self, callback):
890
- self.write(b'Notification-Callback-Context', callback.context)
891
- self.write(b'Notification-Callback-Context-Type',
892
- callback.context_type)
893
-
894
- def write_url_callback(self, callback):
895
- self.write(b'Notification-Callback-Target', callback.url)
896
-
897
- def write(self, name, value):
898
- """Write utf-8 encoded header into writer.
899
-
900
- :param name: the name of the header.
901
- :param value: the value of the header.
902
- """
903
- if isinstance(value, Resource):
904
- value = value.unique_id()
905
- if value is not None:
906
- if not isinstance(value, bytes):
907
- value = text_type(value).encode('utf-8')
908
- self.writer.write(name)
909
- self.writer.write(b': ')
910
- self.writer.write(value)
911
- self.writer.write(LINE_DELIMITER)
912
-
913
-
914
- class SectionPacker(object):
915
-
916
- def __init__(self, cipher):
917
- self.writer = io.BytesIO()
918
- self.cipher = cipher
919
-
920
- def pack(self, request):
921
- """Return utf-8 encoded message body."""
922
- request.write_into(self)
923
- return self.writer.getvalue()
924
-
925
- def write_base_request(self, request):
926
- for _, value in request.custom_headers:
927
- self.write(value)
928
- for _, value in request.app_specific_headers:
929
- self.write(value)
930
-
931
- def write_register_request(self, request):
932
- self.write(request.app_icon)
933
- for event in request.events:
934
- self.write(event.icon)
935
-
936
- def write_notify_request(self, request):
937
- self.write(request.notification.icon)
938
-
939
- def write_subscribe_request(self, request):
940
- pass
941
-
942
- def write(self, resource):
943
- """Write utf-8 encoded resource into writer.
944
-
945
- :param headers: the iterable of (`name`, `value`) tuple of the header.
946
- :param body: bytes of section body.
947
- """
948
- if isinstance(resource, Resource) and resource.data is not None:
949
- data = self.cipher.encrypt(resource.data)
950
- self.writer.write(SECTION_DELIMITER)
951
- self.writer.write(b'Identifier: ')
952
- self.writer.write(resource.unique_value())
953
- self.writer.write(LINE_DELIMITER)
954
- self.writer.write(b'Length: ')
955
- self.writer.write(text_type(len(data)).encode('utf-8'))
956
- self.writer.write(LINE_DELIMITER)
957
- self.writer.write(SECTION_BODY_START)
958
- self.writer.write(data)
959
- self.writer.write(SECTION_BODY_END)
1
+ """This module provides core functionalities of gntplib.
2
+
3
+ gntplib is a Growl Notification Transport Protocol (GNTP_) client library in
4
+ Python.
5
+
6
+ .. _GNTP: http://www.growlforwindows.com/gfw/help/gntp.aspx
7
+ """
8
+
9
+ from __future__ import unicode_literals
10
+ import hashlib
11
+ import io
12
+ import re
13
+ import socket
14
+ import sys
15
+
16
+ from . import keys
17
+ from .compat import text_type
18
+ from .exceptions import GNTPError
19
+
20
+
21
+ __version__ = '0.5'
22
+ __all__ = ['notify', 'publish', 'subscribe', 'Event', 'Notifier', 'Publisher',
23
+ 'RawIcon', 'Resource', 'SocketCallback', 'Subscriber']
24
+
25
+
26
+ SUPPORTED_VERSIONS = ['1.0']
27
+ DEFAULT_PORT = 23053
28
+ DEFAULT_TIMEOUT = 10
29
+ DEFAULT_TTL = 60
30
+ MAX_MESSAGE_SIZE = 4096
31
+ MAX_LINE_SIZE = 1024
32
+ LINE_DELIMITER = b'\r\n'
33
+ SECTION_DELIMITER = SECTION_BODY_START = SECTION_BODY_END = b'\r\n'
34
+ MESSAGE_DELIMITER = b'\r\n\r\n'
35
+ MESSAGE_DELIMITER_SIZE = len(MESSAGE_DELIMITER)
36
+ RESPONSE_INFORMATION_LINE_RE = re.compile(
37
+ b'GNTP/([^ ]+) (-OK|-ERROR|-CALLBACK) NONE')
38
+ CUSTOM_HEADER_PREFIX = 'X-'
39
+ APP_SPECIFIC_HEADER_PREFIX = 'Data-'
40
+
41
+
42
+ def publish(app_name, event_name, title, text=''):
43
+ """Register a publisher and send a notification at a time.
44
+
45
+ :param app_name: the name of the application.
46
+ :param event_name: the name of the notification.
47
+ :param title: the title of the notification.
48
+ :param text: the text of the notification. Defaults to ``''``.
49
+ """
50
+ publisher = Publisher(app_name, coerce_to_events([event_name]))
51
+ publisher.register()
52
+ publisher.publish(event_name, title, text)
53
+
54
+
55
+ def notify(app_name, event_name, title, text=''):
56
+ """Deprecated notify function."""
57
+ import warnings
58
+ warnings.warn('notify function is deprecated, use publish function'
59
+ ' instead', DeprecationWarning, stacklevel=2)
60
+ publish(app_name, event_name, title, text)
61
+
62
+
63
+ def subscribe(id_, name, hub, password, port=DEFAULT_PORT):
64
+ """Send a subscription request and return ttl.
65
+
66
+ :param id_: the unique id of the subscriber.
67
+ :param name: the name of the subscriber.
68
+ :param hub: the subscribed-to machine. If a string is given, it is used as
69
+ a host of the hub and default port number `23053` is used.
70
+ If host-port tuple is given, it is used directly.
71
+ :param password: the password of the subscribed-to machine.
72
+ :param port: the port number of the subscriber. Defaults to `23053`.
73
+ """
74
+ subscriber = Subscriber(id_, name, hub, password, port=port)
75
+ subscriber.subscribe()
76
+ return subscriber.ttl
77
+
78
+
79
+ class BaseApp(object):
80
+ """Base class for applications.
81
+
82
+ :param custom_headers: the list of key-value tuples for Custom Headers.
83
+ :param app_specific_headers: the list of key-value tuples for App-Specific
84
+ Headers.
85
+ :param gntp_client_class: GNTP client class. If it is `None`,
86
+ :class:`GNTPClient` is used. Defaults to `None`.
87
+ """
88
+
89
+ def __init__(self, custom_headers=None, app_specific_headers=None,
90
+ gntp_client_class=None, **kwargs):
91
+ self.custom_headers = custom_headers or []
92
+ self.app_specific_headers = app_specific_headers or []
93
+ if gntp_client_class is None:
94
+ gntp_client_class = GNTPClient
95
+ self.gntp_client = gntp_client_class(**kwargs)
96
+
97
+
98
+ class Publisher(BaseApp):
99
+ """Publisher of Growl Notification Transport Protocol (GNTP).
100
+
101
+ This class supports ``REGISTER`` and ``NOTIFY`` requests. They
102
+ are sent by :meth:`register()` and :meth:`publish()` methods respectively.
103
+ These methods can accept the optional final callback as `callback` keyword
104
+ argument, which run after closing the connection with the GNTP server.
105
+
106
+ `event_defs` is a list of ``str``, ``unicode``, double (of ``str`` and
107
+ ``bool``) or :class:`Event` instance. It is converted to a list of
108
+ :class:`Event` instance as follows:
109
+ ``str`` or ``unicode`` item becomes value of the `name` attribute of
110
+ :class:`Event` instance, whose other attributes are defaults. Double item
111
+ is expanded to (`name`, `enabled`) tuple, and those values are passed to
112
+ :class:`Event` constructor. :class:`Event` instance item is used directly.
113
+
114
+ Optional keyword arguments are passed to the `gntp_client_class`
115
+ constructor.
116
+
117
+ :param name: the name of the application.
118
+ :param event_defs: the definitions of the notifications.
119
+ :param icon: url string or an instance of :class:`Resource` for the icon of
120
+ the application. Defaults to `None`.
121
+ :param custom_headers: the list of key-value tuples for Custom Headers.
122
+ :param app_specific_headers: the list of key-value tuples for App-Specific
123
+ Headers.
124
+ :param gntp_client_class: GNTP client class. If it is `None`,
125
+ :class:`GNTPClient` is used. Defaults to `None`.
126
+
127
+ .. note:: In Growl 1.3.3, `icon` of url string does not work.
128
+ """
129
+
130
+ def __init__(self, name, event_defs, icon=None, custom_headers=None,
131
+ app_specific_headers=None, gntp_client_class=None, **kwargs):
132
+ self.name = name
133
+ self.icon = icon
134
+ self.events = coerce_to_events(event_defs)
135
+ if not self.events:
136
+ raise GNTPError('You have to set at least one notification type')
137
+ BaseApp.__init__(self, custom_headers, app_specific_headers,
138
+ gntp_client_class, **kwargs)
139
+
140
+ def register(self, callback=None):
141
+ """Register this publisher to the GNTP server.
142
+
143
+ :param callback: the callback run after closing the connection with
144
+ the GNTP server. Defaults to `None`.
145
+ """
146
+ request = RegisterRequest(self.name, self.icon, self.events,
147
+ self.custom_headers,
148
+ self.app_specific_headers)
149
+ self.gntp_client.process_request(request, callback)
150
+
151
+ def publish(self, name, title, text='', id_=None, sticky=False,
152
+ priority=0, icon=None, coalescing_id=None, callback=None,
153
+ gntp_callback=None, **socket_callback_options):
154
+ """Send a notification to the GNTP server.
155
+
156
+ :param name: the name of the notification.
157
+ :param title: the title of the notification.
158
+ :param text: the text of the notification. Defaults to `''`.
159
+ :param id_: the unique ID for the notification. If set, this should be
160
+ unique for every request. Defaults to `None`.
161
+ :param sticky: if set to `True`, the notification remains displayed
162
+ until dismissed by the user. Defaults to `False`.
163
+ :param priority: the display hint for the receiver which may be
164
+ ignored. A higher number indicates a higher priority.
165
+ Valid values are between -2 and 2, defaults to `0`.
166
+ :param icon: url string or an instance of :class:`Resource` to display
167
+ with the notification. Defaults to `None`.
168
+ :param coalescing_id: if set, should contain the value of the `id_` of
169
+ a previously-sent notification.
170
+ This serves as a hint to the notification system
171
+ that this notification should replace/update the
172
+ matching previous notification. The notification
173
+ system may ignore this hint. Defaults to `None`.
174
+ :param callback: the callback run after closing the connection with
175
+ the GNTP server. Defaults to `None`.
176
+ :param gntp_callback: url string for url callback or
177
+ :class:`SocketCallback` instance for socket
178
+ callback. Defaults to `None`.
179
+ :param socket_callback_options: the keyword arguments to be used to
180
+ instantiating :class:`SocketCallback`
181
+ for socket callback. About acceptable
182
+ keyword arguments,
183
+ see :class:`SocketCallback`.
184
+
185
+ .. note:: In Growl 1.3.3, `icon` of url string does not work.
186
+
187
+ .. note:: Growl for Windows v2.0+ and Growl v1.3+ require
188
+ `coalescing_id` to be the same on both the original and
189
+ updated notifcation, ignoring the value of `id_`.
190
+ """
191
+ notification = Notification(name, title, text, id_=id_, sticky=sticky,
192
+ priority=priority, icon=icon,
193
+ coalescing_id=coalescing_id,
194
+ gntp_callback=gntp_callback,
195
+ **socket_callback_options)
196
+ request = NotifyRequest(self.name, notification,
197
+ self.custom_headers,
198
+ self.app_specific_headers)
199
+ self.gntp_client.process_request(
200
+ request, callback, socket_callback=notification.socket_callback)
201
+
202
+
203
+ class Notifier(Publisher):
204
+ """Deprecated Notifier of Growl Notification Transport Protocol (GNTP)."""
205
+
206
+ def __init__(self, name, event_defs, icon=None, custom_headers=None,
207
+ app_specific_headers=None, gntp_client_class=None, **kwargs):
208
+ import warnings
209
+ warnings.warn('Notifier is deprecated, use Publisher instead',
210
+ DeprecationWarning, stacklevel=2)
211
+ Publisher.__init__(self, name, event_defs, icon,
212
+ custom_headers, app_specific_headers,
213
+ gntp_client_class, **kwargs)
214
+
215
+ def notify(self, name, title, text='', id_=None, sticky=False,
216
+ priority=0, icon=None, coalescing_id=None, callback=None,
217
+ gntp_callback=None, **socket_callback_options):
218
+ """Send a notification to the GNTP server."""
219
+ import warnings
220
+ warnings.warn('notify method is deprecated, use publish method'
221
+ ' instead', DeprecationWarning, stacklevel=2)
222
+ self.publish(name, title, text, id_, sticky, priority, icon,
223
+ coalescing_id, callback, gntp_callback,
224
+ **socket_callback_options)
225
+
226
+
227
+ class Subscriber(BaseApp):
228
+ """Subscriber of Growl Notification Transport Protocol (GNTP).
229
+
230
+ This class supports ``SUBSCRIBE`` request.
231
+
232
+ :param id_: the unique id of the subscriber.
233
+ :param name: the name of the subscriber.
234
+ :param hub: the subscribed-to machine. If a string is given, it is used as
235
+ a host of the hub and default port number `23053` is used.
236
+ If host-port tuple is given, it is used directly.
237
+ :param password: the password of the subscribed-to machine.
238
+ :param port: the port number of the subscriber. Defaults to `23053`.
239
+ :param custom_headers: the list of key-value tuples for Custom Headers.
240
+ :param app_specific_headers: the list of key-value tuples for App-Specific
241
+ Headers.
242
+ :param gntp_client_class: GNTP client class. If it is `None`,
243
+ :class:`GNTPClient` is used. Defaults to `None`.
244
+ """
245
+
246
+ def __init__(self, id_, name, hub, password, port=DEFAULT_PORT,
247
+ custom_headers=None, app_specific_headers=None,
248
+ gntp_client_class=None, **kwargs):
249
+ self.id_ = id_
250
+ self.name = name
251
+ if isinstance(hub, (bytes, text_type)):
252
+ self.hub = (hub, DEFAULT_PORT)
253
+ else:
254
+ self.hub = hub
255
+ self.password = password
256
+ self.port = port
257
+ self.ttl = DEFAULT_TTL
258
+ BaseApp.__init__(self, custom_headers, app_specific_headers,
259
+ gntp_client_class, host=self.hub[0], port=self.hub[1],
260
+ password=self.password, **kwargs)
261
+
262
+ def subscribe(self, callback=None):
263
+ """Send a subscription request.
264
+
265
+ If `callback` is `None`, :meth:`store_ttl` is used and :attr:`ttl` is
266
+ updated by ``Subscription-TTL`` value of the response.
267
+
268
+ :param callback: the callback run after closing the connection with
269
+ the GNTP server. Defaults to `None`.
270
+ """
271
+ request = SubscribeRequest(self.id_, self.name, self.port,
272
+ self.custom_headers,
273
+ self.app_specific_headers)
274
+ self.gntp_client.process_request(request, callback or self.store_ttl)
275
+
276
+ def store_ttl(self, response):
277
+ """Update :attr:`ttl` attribute."""
278
+ ttl = response.headers['Subscription-TTL']
279
+ self.ttl = int(ttl)
280
+
281
+
282
+ class Event(object):
283
+ """Represent notification type.
284
+
285
+ :param name: the name of the notification.
286
+ :param display_name: the display name of the notification, which is
287
+ appeared at the Applications tab of the Growl
288
+ Preferences. Defaults to `None`.
289
+ :param enabled: indicates if the notification should be enabled by
290
+ default. Defaults to `True`.
291
+ :param icon: url string or an instance of :class:`Resource` for the default
292
+ icon to display with the notifications of this notification
293
+ type. Defaults to `None`.
294
+
295
+ .. note:: In Growl 1.3.3, `icon` does not work.
296
+ """
297
+
298
+ def __init__(self, name, display_name=None, enabled=True, icon=None):
299
+ self.name = name
300
+ self.display_name = display_name
301
+ self.enabled = enabled
302
+ self.icon = icon
303
+
304
+
305
+ class Notification(object):
306
+ """Represent notification."""
307
+
308
+ def __init__(self, name, title, text='', id_=None, sticky=None,
309
+ priority=None, icon=None, coalescing_id=None,
310
+ gntp_callback=None, **socket_callback_options):
311
+ self.name = name
312
+ self.title = title
313
+ self.text = text
314
+ self.id_ = id_
315
+ self.sticky = sticky
316
+ self.priority = priority
317
+ self.icon = icon
318
+ self.coalescing_id = coalescing_id
319
+ self.callback = coerce_to_callback(gntp_callback,
320
+ **socket_callback_options)
321
+
322
+ @property
323
+ def socket_callback(self):
324
+ if isinstance(self.callback, SocketCallback):
325
+ return self.callback
326
+
327
+
328
+ class BaseRequest(object):
329
+ """Abstract base class for GNTP request.
330
+
331
+ :param custom_headers: the list of key-value tuples for Custom Headers.
332
+ :param app_specific_headers: the list of key-value tuples for App-Specific
333
+ Headers.
334
+ """
335
+ #: Request message type. Subclasses must override this attribute.
336
+ message_type = None
337
+
338
+ def __init__(self, custom_headers=None, app_specific_headers=None):
339
+ self.custom_headers = custom_headers or []
340
+ self.app_specific_headers = app_specific_headers or []
341
+
342
+ def write_into(self, writer):
343
+ """Subclasses must call this method first to serialize their
344
+ message."""
345
+ writer.write_base_request(self)
346
+
347
+
348
+ class RegisterRequest(BaseRequest):
349
+ """Represent ``REGISTER`` request.
350
+
351
+ :param app_name: the name of the application.
352
+ :param app_icon: url string or an instance of :class:`Resource` for the
353
+ icon of the application.
354
+ :param events: the list of :class:`Event` instances.
355
+ :param custom_headers: the list of key-value tuples for Custom Headers.
356
+ :param app_specific_headers: the list of key-value tuples for App-Specific
357
+ Headers.
358
+
359
+ .. note:: In Growl 1.3.3, `app_icon` of url string does not work.
360
+ """
361
+ message_type = 'REGISTER'
362
+
363
+ def __init__(self, app_name, app_icon, events, custom_headers=None,
364
+ app_specific_headers=None):
365
+ BaseRequest.__init__(self, custom_headers, app_specific_headers)
366
+ self.app_name = app_name
367
+ self.app_icon = app_icon
368
+ self.events = events
369
+
370
+ def write_into(self, writer):
371
+ BaseRequest.write_into(self, writer)
372
+ writer.write_register_request(self)
373
+
374
+
375
+ class NotifyRequest(BaseRequest):
376
+ """Represent ``NOTIFY`` request.
377
+
378
+ :param app_name: the name of the application.
379
+ :param notification: :class:`Notification` instance.
380
+ :param custom_headers: the list of key-value tuples for Custom Headers.
381
+ :param app_specific_headers: the list of key-value tuples for App-Specific
382
+ Headers.
383
+ """
384
+ message_type = 'NOTIFY'
385
+
386
+ def __init__(self, app_name, notification, custom_headers=None,
387
+ app_specific_headers=None):
388
+ BaseRequest.__init__(self, custom_headers, app_specific_headers)
389
+ self.app_name = app_name
390
+ self.notification = notification
391
+
392
+ def write_into(self, writer):
393
+ BaseRequest.write_into(self, writer)
394
+ writer.write_notify_request(self)
395
+
396
+
397
+ class SubscribeRequest(BaseRequest):
398
+ """Represent ``SUBSCRIBE`` request.
399
+
400
+ :param id_: the unique id of the subscriber.
401
+ :param name: the name of the subscriber.
402
+ :param port: the port number of the subscriber.
403
+ :param custom_headers: the list of key-value tuples for Custom Headers.
404
+ :param app_specific_headers: the list of key-value tuples for App-Specific
405
+ Headers.
406
+ """
407
+ message_type = 'SUBSCRIBE'
408
+
409
+ def __init__(self, id_, name, port, custom_headers=None,
410
+ app_specific_headers=None):
411
+ BaseRequest.__init__(self, custom_headers, app_specific_headers)
412
+ self.id_ = id_
413
+ self.name = name
414
+ self.port = port
415
+
416
+ def write_into(self, writer):
417
+ BaseRequest.write_into(self, writer)
418
+ writer.write_subscribe_request(self)
419
+
420
+
421
+ class Response(object):
422
+ """Base class for GNTP response.
423
+
424
+ :param message_type: <messagetype> of the response. `'-OK'`, `'-ERROR'` or
425
+ `'-CALLBACK'`.
426
+ :param headers: headers of the response.
427
+ """
428
+
429
+ def __init__(self, message_type, headers):
430
+ self.message_type = message_type
431
+ self.headers = headers
432
+
433
+
434
+ class BaseGNTPConnection(object):
435
+ """Abstract base class for GNTP connection."""
436
+
437
+ def __init__(self, final_callback, socket_callback=None):
438
+ self.final_callback = final_callback
439
+ self.socket_callback = socket_callback
440
+
441
+ def on_ok_message(self, message):
442
+ r"""Callback for ``-OK`` response.
443
+
444
+ :param message: string of response terminated by `'\\r\\n\\r\\n'`.
445
+ """
446
+ try:
447
+ response = parse_response(message, '-OK')
448
+ if self.socket_callback is not None:
449
+ self.read_message(self.on_callback_message)
450
+ finally:
451
+ if self.socket_callback is None:
452
+ self.close()
453
+ if self.socket_callback is None and self.final_callback is not None:
454
+ self.final_callback(response)
455
+
456
+ def on_callback_message(self, message):
457
+ r"""Callback for ``-CALLBACK`` response.
458
+
459
+ :param message: string of response terminated by `'\\r\\n\\r\\n'`.
460
+ """
461
+ try:
462
+ response = parse_response(message, '-CALLBACK')
463
+ callback_result = self.socket_callback(response)
464
+ finally:
465
+ self.close()
466
+ if self.final_callback is not None:
467
+ self.final_callback(callback_result)
468
+
469
+ def write_message(self, message):
470
+ """Subclasses must override this method to send a message to the GNTP
471
+ server."""
472
+ raise NotImplementedError
473
+
474
+ def read_message(self, callback):
475
+ """Subclasses must override this method to receive a message from the
476
+ GNTP server."""
477
+ raise NotImplementedError
478
+
479
+ def close(self):
480
+ """Subclasses must override this method to close the connection with
481
+ the GNTP server."""
482
+ raise NotImplementedError
483
+
484
+
485
+ class GNTPConnection(BaseGNTPConnection):
486
+ """Represent the connection with the GNTP server."""
487
+
488
+ def __init__(self, address, timeout, final_callback, socket_callback=None):
489
+ BaseGNTPConnection.__init__(self, final_callback, socket_callback)
490
+ self.sock = socket.create_connection(address, timeout=timeout)
491
+
492
+ def write_message(self, message):
493
+ """Send the request message to the GNTP server."""
494
+ self.sock.send(message)
495
+
496
+ def read_message(self, callback):
497
+ """Read a message from opened socket and run callback with it."""
498
+ message = next(generate_messages(self.sock))
499
+ callback(message)
500
+
501
+ def close(self):
502
+ """Close the socket."""
503
+ self.sock.close()
504
+ self.sock = None
505
+
506
+
507
+ class GNTPClient(object):
508
+ """GNTP client.
509
+
510
+ :param host: host of GNTP server. Defaults to `'localhost'`.
511
+ :param port: port of GNTP server. Defaults to `23053`.
512
+ :param timeout: timeout in seconds. Defaults to `10`.
513
+ :param password: the password used in creating the key.
514
+ :param key_hashing: the type of hash algorithm used in creating the key.
515
+ It is `keys.MD5`, `keys.SHA1`, `keys.SHA256` or
516
+ `keys.SHA512`. Defaults to `keys.SHA256`.
517
+ :param encryption: the tyep of encryption algorithm used.
518
+ It is `None`, `ciphers.AES`, `ciphers.DES` or
519
+ `ciphers.3DES`. `None` means no encryption.
520
+ Defaults to `None`.
521
+ :param connection_class: GNTP connection class. If it is `None`,
522
+ :class:`GNTPConnection` is used. Defaults to
523
+ `None`.
524
+ """
525
+
526
+ def __init__(self, host='localhost', port=DEFAULT_PORT,
527
+ timeout=DEFAULT_TIMEOUT, password=None,
528
+ key_hashing=keys.SHA256, encryption=None,
529
+ connection_class=None):
530
+ self.address = (host, port)
531
+ self.timeout = timeout
532
+ self.connection_class = connection_class or GNTPConnection
533
+ if (encryption is not None and
534
+ encryption.key_size > key_hashing.key_size):
535
+ raise GNTPError('key_hashing key size (%s:%d) must be at'
536
+ ' least encryption key size (%s:%d)' % (
537
+ key_hashing.algorithm_id, key_hashing.key_size,
538
+ encryption.algorithm_id, encryption.key_size))
539
+ self.packer_factory = MessagePackerFactory(password, key_hashing,
540
+ encryption)
541
+
542
+ def process_request(self, request, callback, **kwargs):
543
+ """Process a request.
544
+
545
+ :param callback: the final callback run after closing connection.
546
+ """
547
+ packer = self.packer_factory.create()
548
+ message = packer.pack(request)
549
+ conn = self._connect(callback, **kwargs)
550
+ conn.write_message(message)
551
+ conn.read_message(conn.on_ok_message)
552
+
553
+ def _connect(self, final_callback, **kwargs):
554
+ """Connect to the GNTP server and return the connection."""
555
+ return self.connection_class(self.address, self.timeout,
556
+ final_callback, **kwargs)
557
+
558
+
559
+ def generate_messages(sock, size=1024):
560
+ """Generate messages from opened socket."""
561
+ buf = b''
562
+ while True:
563
+ buf += sock.recv(size)
564
+ if not buf:
565
+ break
566
+ pos = buf.find(MESSAGE_DELIMITER)
567
+ if ((pos < 0 and len(buf) >= MAX_MESSAGE_SIZE) or
568
+ (pos > MAX_MESSAGE_SIZE - MESSAGE_DELIMITER_SIZE)):
569
+ raise GNTPError('too large message: %r' % buf)
570
+ elif pos > 0:
571
+ pos += 4
572
+ yield buf[:pos]
573
+ buf = buf[pos:]
574
+
575
+
576
+ def parse_response(message, expected_message_type=None):
577
+ """Parse response and return response object."""
578
+ try:
579
+ lines = [line for line in message.split(LINE_DELIMITER) if line]
580
+ _, message_type = parse_information_line(lines.pop(0))
581
+ if (expected_message_type is not None and
582
+ expected_message_type != message_type):
583
+ raise GNTPError('%s is not expected message type %s' % (
584
+ message_type, expected_message_type))
585
+
586
+ headers = dict([s.strip().decode('utf-8') for s in line.split(b':', 1)]
587
+ for line in lines)
588
+ if message_type == '-ERROR':
589
+ raise GNTPError('%s: %s' % (headers['Error-Code'],
590
+ headers['Error-Description']))
591
+ return Response(message_type, headers)
592
+ except ValueError as exc:
593
+ raise GNTPError(exc.args[0], 'original message: %r' % message)
594
+ except GNTPError as exc:
595
+ exc.args = (exc.args[0], 'original message: %r' % message)
596
+ raise exc
597
+
598
+
599
+ def parse_information_line(line):
600
+ """Parse information line and return tuple (`<version>`,
601
+ `<messagetype>`)."""
602
+ matched = RESPONSE_INFORMATION_LINE_RE.match(line)
603
+ if matched is None:
604
+ raise GNTPError('invalid information line: %r' % line)
605
+ version, message_type = [s.decode('utf-8') for s in matched.groups()]
606
+ if version not in SUPPORTED_VERSIONS:
607
+ raise GNTPError("version '%s' is not supported" % version)
608
+ return version, message_type
609
+
610
+
611
+ def coerce_to_events(items):
612
+ """Coerce the list of the event definitions to the list of :class:`Event`
613
+ instances."""
614
+ results = []
615
+ for item in items:
616
+ if isinstance(item, (bytes, text_type)):
617
+ results.append(Event(item, enabled=True))
618
+ elif isinstance(item, tuple):
619
+ name, enabled = item
620
+ results.append(Event(name, enabled=enabled))
621
+ elif isinstance(item, Event):
622
+ results.append(item)
623
+ return results
624
+
625
+
626
+ class Resource(object):
627
+ """Class for <uniqueid> data types.
628
+
629
+ :param data: the binary content.
630
+ """
631
+
632
+ def __init__(self, data):
633
+ self.data = data
634
+ self._unique_value = None
635
+
636
+ def unique_value(self):
637
+ """Return the <uniquevalue> value."""
638
+ if self.data is not None and self._unique_value is None:
639
+ try:
640
+ self._unique_value = hashlib.md5(self.data).hexdigest().encode('utf-8')
641
+ except:
642
+ self._unique_value = hashlib.md5(self.data.encode('utf-8')).hexdigest().encode('utf-8')
643
+ return self._unique_value
644
+
645
+ def unique_id(self):
646
+ """Return the <uniqueid> value."""
647
+ if self.data is not None:
648
+ return b'x-growl-resource://' + self.unique_value()
649
+
650
+
651
+ class RawIcon(Resource):
652
+ """Deprecated icon class."""
653
+
654
+ def __init__(self, data):
655
+ import warnings
656
+ warnings.warn('RawIcon is deprecated, use Resource instead',
657
+ DeprecationWarning, stacklevel=2)
658
+ Resource.__init__(self, data)
659
+
660
+
661
+ class SocketCallback(object):
662
+ """Base class for socket callback.
663
+
664
+ Each of the callbacks takes one positional argument, which is
665
+ :class:`Response` instance.
666
+
667
+ :param context: value of ``Notification-Callback-Context``.
668
+ Defaults to ``'None'``.
669
+ :param context-type: value of ``Notification-Callback-Context-Type``.
670
+ Defaults to ``'None'``.
671
+ :param on_click: the callback run at ``CLICKED`` callback result.
672
+ :param on_close: the callback run at ``CLOSED`` callback result.
673
+ :param on_timeout: the callback run at ``TIMEDOUT`` callback result.
674
+
675
+ .. note:: TIMEDOUT callback does not occur in my Growl 1.3.3.
676
+ """
677
+
678
+ def __init__(self, context='None', context_type='None',
679
+ on_click=None, on_close=None, on_timeout=None):
680
+ self.context = context
681
+ self.context_type = context_type
682
+ self.on_click_callback = on_click
683
+ self.on_close_callback = on_close
684
+ self.on_timeout_callback = on_timeout
685
+
686
+ def on_click(self, response):
687
+ """Run ``CLICKED`` event callback."""
688
+ if self.on_click_callback is not None:
689
+ return self.on_click_callback(response)
690
+
691
+ def on_close(self, response):
692
+ """Run ``CLOSED`` event callback."""
693
+ if self.on_close_callback is not None:
694
+ return self.on_close_callback(response)
695
+
696
+ def on_timeout(self, response):
697
+ """Run ``TIMEDOUT`` event callback."""
698
+ if self.on_timeout_callback is not None:
699
+ return self.on_timeout_callback(response)
700
+
701
+ def __call__(self, response):
702
+ """This is the callback. Delegate to ``on_`` methods depending on
703
+ ``Notification-Callback-Result`` value.
704
+
705
+ :param response: :class:`Response` instance.
706
+ """
707
+ callback_result = response.headers['Notification-Callback-Result']
708
+ delegate_map = {
709
+ 'CLICKED': self.on_click, 'CLICK': self.on_click,
710
+ 'CLOSED': self.on_close, 'CLOSE': self.on_close,
711
+ 'TIMEDOUT': self.on_timeout, 'TIMEOUT': self.on_timeout,
712
+ }
713
+ return delegate_map[callback_result](response)
714
+
715
+ def write_into(self, writer):
716
+ writer.write_socket_callback(self)
717
+
718
+
719
+ class URLCallback(object):
720
+ """Class for url callback."""
721
+
722
+ def __init__(self, url):
723
+ self.url = url
724
+
725
+ def write_into(self, writer):
726
+ writer.write_url_callback(self)
727
+
728
+
729
+ def coerce_to_callback(gntp_callback=None, **socket_callback_options):
730
+ """Return :class:`URLCallback` instance for url callback or
731
+ :class:`SocketCallback` instance for socket callback.
732
+
733
+ If `gntp_callback` is not `None`, `socket_callback_options` must be empty.
734
+ Moreover, if `gntp_callback` is string, then a instance of
735
+ :class:`URLCallback` is returned. Otherwise, `gntp_callback` is returned
736
+ directly.
737
+
738
+ If `gntp_callback` is `None` and `socket_callback_options` is not empty,
739
+ new instance of :class:`SocketCallback` is created from given keyword
740
+ arguments and it is returned. Acceptable keyword arguments are same as
741
+ constructor's of :class:`SocketCallback`.
742
+ """
743
+ if gntp_callback is not None:
744
+ if socket_callback_options:
745
+ raise GNTPError('If gntp_callback is not None,'
746
+ ' socket_callback_options must be empty')
747
+ if isinstance(gntp_callback, (bytes, text_type)):
748
+ return URLCallback(gntp_callback)
749
+ else:
750
+ return gntp_callback
751
+ if socket_callback_options:
752
+ return SocketCallback(**socket_callback_options)
753
+
754
+
755
+ class _NullCipher(object):
756
+ """Null object for the encryption of messages."""
757
+
758
+ algorithm = None
759
+ algorithm_id = 'NONE'
760
+ encrypt = lambda self, text: text
761
+ decrypt = lambda self, text: text
762
+ __bool__ = lambda self: False
763
+ __nonzero__ = __bool__
764
+
765
+
766
+ NullCipher = _NullCipher()
767
+
768
+
769
+ class MessagePackerFactory(object):
770
+ """The factory of :class:`MessagePacker`.
771
+
772
+ If `password` is None, `hashing` and `encryption` are ignored.
773
+ """
774
+
775
+ def __init__(self, password=None, hashing=keys.SHA256, encryption=None):
776
+ self.password = password
777
+ self.hashing = password and hashing
778
+ self.encryption = (password and encryption) or NullCipher
779
+
780
+ def create(self):
781
+ """Create an instance of :class:`MessagePacker` and return it."""
782
+ key = self.password and self.hashing.key(self.password)
783
+ cipher = self.encryption and self.encryption.cipher(key)
784
+ return MessagePacker(key, cipher)
785
+
786
+
787
+ class MessagePacker(object):
788
+ """The serializer for messages.
789
+
790
+ `key` and `cipher` have random-generated salt and iv respectively.
791
+
792
+ :param key: an instance of :class:`keys.Key`.
793
+ :param cipher: an instance of :class:`ciphers.Cipher` or `NullCipher`.
794
+ """
795
+
796
+ def __init__(self, key=None, cipher=None):
797
+ self.key = key
798
+ self.cipher = cipher or NullCipher
799
+
800
+ def pack(self, request):
801
+ """Return utf-8 encoded request message."""
802
+ return (InformationLinePacker(self.key, self.cipher).pack(request) +
803
+ LINE_DELIMITER +
804
+ HeaderPacker(self.cipher).pack(request) +
805
+ SectionPacker(self.cipher).pack(request) +
806
+ LINE_DELIMITER)
807
+
808
+
809
+ class InformationLinePacker(object):
810
+
811
+ def __init__(self, key, cipher):
812
+ self.key = key
813
+ self.cipher = cipher
814
+
815
+ def pack(self, request):
816
+ """Return utf-8 encoded information line."""
817
+ result = (b'GNTP/1.0 ' +
818
+ request.message_type.encode('utf-8') +
819
+ b' ' +
820
+ self.cipher.algorithm_id.encode('utf-8'))
821
+ if self.cipher.algorithm is not None:
822
+ result += b':' + self.cipher.iv_hex
823
+ if self.key is not None:
824
+ result += (b' ' +
825
+ self.key.algorithm_id.encode('utf-8') +
826
+ b':' +
827
+ self.key.key_hash_hex +
828
+ b'.' +
829
+ self.key.salt_hex)
830
+ return result
831
+
832
+
833
+ class HeaderPacker(object):
834
+
835
+ def __init__(self, cipher):
836
+ self.writer = io.BytesIO()
837
+ self.cipher = cipher
838
+
839
+ def pack(self, request):
840
+ """Return utf-8 encoded headers."""
841
+ request.write_into(self)
842
+ headers = self.writer.getvalue()
843
+ result = self.cipher.encrypt(headers)
844
+ if self.cipher.algorithm is not None:
845
+ result += LINE_DELIMITER
846
+ return result
847
+
848
+ def write_base_request(self, request):
849
+ self._write_additional_headers(request.custom_headers,
850
+ CUSTOM_HEADER_PREFIX)
851
+ self._write_additional_headers(request.app_specific_headers,
852
+ APP_SPECIFIC_HEADER_PREFIX)
853
+
854
+ def _write_additional_headers(self, headers, prefix):
855
+ for key, value in headers:
856
+ if not key.startswith(prefix):
857
+ key = prefix + key
858
+ self.write(key.encode('utf-8'), value)
859
+
860
+ def write_register_request(self, request):
861
+ self.write(b'Application-Name', request.app_name)
862
+ self.write(b'Application-Icon', request.app_icon)
863
+ self.write(b'Notifications-Count', len(request.events))
864
+ for event in request.events:
865
+ self.writer.write(LINE_DELIMITER)
866
+ self.write(b'Notification-Name', event.name)
867
+ self.write(b'Notification-Display-Name', event.display_name)
868
+ self.write(b'Notification-Enabled', event.enabled)
869
+ self.write(b'Notification-Icon', event.icon)
870
+
871
+ def write_notify_request(self, request):
872
+ self.write(b'Application-Name', request.app_name)
873
+ self._write_notification(request.notification)
874
+
875
+ def write_subscribe_request(self, request):
876
+ self.write(b'Subscriber-ID', request.id_)
877
+ self.write(b'Subscriber-Name', request.name)
878
+ self.write(b'Subscriber-Port', request.port)
879
+
880
+ def _write_notification(self, notification):
881
+ self.write(b'Notification-Name', notification.name)
882
+ self.write(b'Notification-ID', notification.id_)
883
+ self.write(b'Notification-Title', notification.title)
884
+ self.write(b'Notification-Text', notification.text)
885
+ self.write(b'Notification-Sticky', notification.sticky)
886
+ self.write(b'Notification-Priority', notification.priority)
887
+ self.write(b'Notification-Icon', notification.icon)
888
+ self.write(b'Notification-Coalescing-ID', notification.coalescing_id)
889
+ if notification.callback is not None:
890
+ notification.callback.write_into(self)
891
+
892
+ def write_socket_callback(self, callback):
893
+ self.write(b'Notification-Callback-Context', callback.context)
894
+ self.write(b'Notification-Callback-Context-Type',
895
+ callback.context_type)
896
+
897
+ def write_url_callback(self, callback):
898
+ self.write(b'Notification-Callback-Target', callback.url)
899
+
900
+ def write(self, name, value):
901
+ """Write utf-8 encoded header into writer.
902
+
903
+ :param name: the name of the header.
904
+ :param value: the value of the header.
905
+ """
906
+ if isinstance(value, Resource):
907
+ value = value.unique_id()
908
+ if value is not None:
909
+ if not isinstance(value, bytes):
910
+ value = text_type(value).encode('utf-8')
911
+ self.writer.write(name)
912
+ self.writer.write(b': ')
913
+ self.writer.write(value)
914
+ self.writer.write(LINE_DELIMITER)
915
+
916
+
917
+ class SectionPacker(object):
918
+
919
+ def __init__(self, cipher):
920
+ self.writer = io.BytesIO()
921
+ self.cipher = cipher
922
+
923
+ def pack(self, request):
924
+ """Return utf-8 encoded message body."""
925
+ request.write_into(self)
926
+ return self.writer.getvalue()
927
+
928
+ def write_base_request(self, request):
929
+ for _, value in request.custom_headers:
930
+ self.write(value)
931
+ for _, value in request.app_specific_headers:
932
+ self.write(value)
933
+
934
+ def write_register_request(self, request):
935
+ self.write(request.app_icon)
936
+ for event in request.events:
937
+ self.write(event.icon)
938
+
939
+ def write_notify_request(self, request):
940
+ self.write(request.notification.icon)
941
+
942
+ def write_subscribe_request(self, request):
943
+ pass
944
+
945
+ def write(self, resource):
946
+ """Write utf-8 encoded resource into writer.
947
+
948
+ :param headers: the iterable of (`name`, `value`) tuple of the header.
949
+ :param body: bytes of section body.
950
+ """
951
+ if isinstance(resource, Resource) and resource.data is not None:
952
+ data = self.cipher.encrypt(resource.data)
953
+ self.writer.write(SECTION_DELIMITER)
954
+ self.writer.write(b'Identifier: ')
955
+ self.writer.write(resource.unique_value())
956
+ self.writer.write(LINE_DELIMITER)
957
+ self.writer.write(b'Length: ')
958
+ self.writer.write(text_type(len(data)).encode('utf-8'))
959
+ self.writer.write(LINE_DELIMITER)
960
+ self.writer.write(SECTION_BODY_START)
961
+ if sys.version_info.major < 3:
962
+ self.writer.write(data)
963
+ else:
964
+ # print("DATA =", data)
965
+ # print("type(DATA) =", type(data))
966
+ if sys.version_info.major == 2:
967
+ self.writer.write(data)
968
+ else:
969
+ if isinstance(data, bytes):
970
+ self.writer.write(data)
971
+ else:
972
+ self.writer.write(bytes(data, encoding='utf-8'))
973
+ self.writer.write(SECTION_BODY_END)