Skip to content

Commit c757a83

Browse files
committed
Merge branch '21st-century'
2 parents c768026 + 25b746f commit c757a83

File tree

8 files changed

+140
-228
lines changed

8 files changed

+140
-228
lines changed

CHANGES.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
version 0.4.0 - 2012-02-14
2+
==========================
3+
4+
New:
5+
6+
* Client interface methods have more explicit arguments and now take an
7+
error callback.
8+
9+
The `provision` `notify` and `feedback` methods in v0.3.2 would
10+
implicitly assume they should perform asynchronously if the `callback`
11+
argument was supplied. They would spin off a thread, later calling
12+
the callback.
13+
14+
Now those methods take an `async` parameter, when True, will spin off
15+
the XML-RPC request in another thread, calling `callback(result)` when
16+
complete or `errback(exception)` if an error occurred.
17+
18+
* Clients will now automatically try to re-provision themselves if `INITIAL`
19+
was provided during configuration. This helps, in case the server goes
20+
down, to be able to retry provisioning at least once.
21+
22+
Fixed bugs:
23+
24+
* Fixed #21 - logging of full contents of file PEM file when providing the
25+
whole thing in `provision()`
26+
27+
* Fixed #25 - provide a wrapper for api functions which will automaticaly
28+
try to reprovision should it encounter a `UknownAppID`.
29+
30+
Other:
31+
32+
* Removed some of the redundant documentation from the setup.py file.
33+
Eventually I want to move to sphynx docs. That software didn't really
34+
exist in the same capacity in 2010 as it does now.
35+
36+
version 0.3.2 - 2010-08-05
37+
==========================
38+
39+
Started tracking changes. This is the version that everyone has been using for
40+
the past few years.
41+

example_tac.tac

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import twisted.application, twisted.web, twisted.application.internet
2+
import pyapns.server
3+
4+
application = twisted.application.service.Application("pyapns application")
5+
6+
resource = twisted.web.resource.Resource()
7+
resource.putChild('', pyapns.server.APNSServer())
8+
site = twisted.web.server.Site(resource)
9+
10+
server = twisted.application.internet.TCPServer(7077, site)
11+
server.setServiceParent(application)
12+

pyapns/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.pyc
2+
*.log
3+
*.pid
4+

pyapns/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .client import notify, provision, feedback, configure
1+
from .client import notify, provision, feedback, configure__version__ = "0.4.0"__author__ = "Samuel Sutch"__license__ = "MIT"__copyright__ = "Copyrighit 2012 Samuel Sutch"

pyapns/client.py

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import xmlrpclib
22
import threading
33
import httplib
4+
import functools
45
from sys import hexversion
56

67
OPTIONS = {'CONFIGURED': False, 'TIMEOUT': 20}
@@ -40,32 +41,76 @@ def configure(opts):
4041
class UnknownAppID(Exception): pass
4142
class APNSNotConfigured(Exception): pass
4243

44+
def reprovision_and_retry(func):
45+
"""
46+
Wraps the `errback` callback of the API functions, automatically trying to
47+
re-provision if the app ID can not be found during the operation. If that's
48+
unsuccessful, it will raise the UnknownAppID error.
49+
"""
50+
@functools.wraps(func)
51+
def wrapper(*a, **kw):
52+
errback = kw.get('errback', None)
53+
if errback is None:
54+
def errback(e):
55+
raise e
56+
def errback_wrapper(e):
57+
if isinstance(e, UnknownAppID) and 'INITIAL' in OPTIONS:
58+
try:
59+
for initial in OPTIONS['INITIAL']:
60+
provision(*initial) # retry provisioning the initial setup
61+
func(*a, **kw) # and try the function once more
62+
except Exception, new_exc:
63+
errback(new_exc) # throwing the new exception
64+
else:
65+
errback(e) # not an instance of UnknownAppID - nothing we can do here
66+
kw['errback'] = errback_wrapper
67+
return func(*a, **kw)
68+
return wrapper
69+
70+
def default_callback(func):
71+
@functools.wraps(func)
72+
def wrapper(*a, **kw):
73+
if 'callback' not in kw:
74+
kw['callback'] = lambda c: c
75+
return func(*a, **kw)
76+
return wrapper
4377

44-
def provision(app_id, path_to_cert, environment, timeout=15, callback=None):
78+
@default_callback
79+
@reprovision_and_retry
80+
def provision(app_id, path_to_cert, environment, timeout=15, async=False,
81+
callback=None, errback=None):
4582
args = [app_id, path_to_cert, environment, timeout]
46-
if callback is None:
47-
return _xmlrpc_thread('provision', args, lambda r: r)
48-
t = threading.Thread(target=_xmlrpc_thread, args=['provision', args, callback])
83+
f_args = ['provision', args, callback, errback]
84+
if not async:
85+
return _xmlrpc_thread(*f_args)
86+
t = threading.Thread(target=_xmlrpc_thread, args=f_args)
4987
t.daemon = True
5088
t.start()
5189

52-
def notify(app_id, tokens, notifications, callback=None):
90+
@default_callback
91+
@reprovision_and_retry
92+
def notify(app_id, tokens, notifications, async=False, callback=None,
93+
errback=None):
5394
args = [app_id, tokens, notifications]
54-
if callback is None:
55-
return _xmlrpc_thread('notify', args, lambda r: r)
56-
t = threading.Thread(target=_xmlrpc_thread, args=['notify', args, callback])
95+
f_args = ['notify', args, callback, errback]
96+
if not async:
97+
return _xmlrpc_thread(*f_args)
98+
t = threading.Thread(target=_xmlrpc_thread, args=f_args)
5799
t.daemon = True
58100
t.start()
59101

60-
def feedback(app_id, callback=None):
102+
@default_callback
103+
@reprovision_and_retry
104+
def feedback(app_id, async=False, callback=None, errback=None):
61105
args = [app_id]
62-
if callback is None:
63-
return _xmlrpc_thread('feedback', args, lambda r: r)
64-
t = threading.Thread(target=_xmlrpc_thread, args=['feedback', args, callback])
106+
f_args = ['feedback', args, callback, errback]
107+
if not async:
108+
return _xmlrpc_thread(*f_args)
109+
t = threading.Thread(target=_xmlrpc_thread, args=f_args)
65110
t.daemon = True
66111
t.start()
67112

68-
def _xmlrpc_thread(method, args, callback):
113+
def _xmlrpc_thread(method, args, callback, errback=None):
69114
if not configure({}):
70115
raise APNSNotConfigured('APNS Has not been configured.')
71116
proxy = ServerProxy(OPTIONS['HOST'], allow_none=True, use_datetime=True,
@@ -77,8 +122,11 @@ def _xmlrpc_thread(method, args, callback):
77122
return callback(proxy(*args))
78123
except xmlrpclib.Fault, e:
79124
if e.faultCode == 404:
80-
raise UnknownAppID
81-
raise
125+
e = UnknownAppID()
126+
if errback is not None:
127+
errback(e)
128+
else:
129+
raise e
82130

83131

84132
## --------------------------------------------------------------

pyapns/server.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from __future__ import with_statement
22
import _json as json
3-
import base64
43
import struct
5-
import logging
64
import binascii
75
import datetime
86
from StringIO import StringIO as _StringIO
@@ -11,7 +9,7 @@
119
from twisted.internet.protocol import (
1210
ReconnectingClientFactory, ClientFactory, Protocol, ServerFactory)
1311
from twisted.internet.ssl import ClientContextFactory
14-
from twisted.application import internet, service
12+
from twisted.application import service
1513
from twisted.protocols.basic import LineReceiver
1614
from twisted.python import log
1715
from zope.interface import Interface, implements
@@ -52,7 +50,10 @@ def read(self):
5250

5351
class APNSClientContextFactory(ClientContextFactory):
5452
def __init__(self, ssl_cert_file):
55-
log.msg('APNSClientContextFactory ssl_cert_file=%s' % ssl_cert_file)
53+
if 'BEGIN CERTIFICATE' not in ssl_cert_file:
54+
log.msg('APNSClientContextFactory ssl_cert_file=%s' % ssl_cert_file)
55+
else:
56+
log.msg('APNSClientContextFactory ssl_cert_file={FROM_STRING}')
5657
self.ctx = SSL.Context(SSL.SSLv3_METHOD)
5758
if 'BEGIN CERTIFICATE' in ssl_cert_file:
5859
cer = crypto.load_certificate(crypto.FILETYPE_PEM, ssl_cert_file)

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Twisted>=8.2.0
2+
pyOpenSSL>=0.10

0 commit comments

Comments
 (0)