Merge pull request #1 from pocketlabs/remove_hyper

remove hyper from apns2
This commit is contained in:
Ankit Desai
2020-06-23 22:11:32 -04:00
committed by GitHub
3 changed files with 32 additions and 60 deletions
+29 -33
View File
@@ -1,4 +1,5 @@
import collections import collections
import httpx
import json import json
import logging import logging
import time import time
@@ -8,7 +9,7 @@ from enum import Enum
from threading import Thread from threading import Thread
from typing import Dict, Iterable, Optional, Tuple, Union from typing import Dict, Iterable, Optional, Tuple, Union
from .credentials import CertificateCredentials, Credentials from .credentials import TokenCredentials
from .errors import ConnectionFailed, exception_class_for_reason from .errors import ConnectionFailed, exception_class_for_reason
# We don't generally need to know about the Credentials subclasses except to # We don't generally need to know about the Credentials subclasses except to
# keep the old API, where APNsClient took a cert_file # keep the old API, where APNsClient took a cert_file
@@ -29,7 +30,7 @@ class NotificationType(Enum):
MDM = 'mdm' MDM = 'mdm'
RequestStream = collections.namedtuple('RequestStream', ['stream_id', 'token']) RequestStream = collections.namedtuple('RequestStream', ['token', 'status', 'reason'])
Notification = collections.namedtuple('Notification', ['token', 'payload']) Notification = collections.namedtuple('Notification', ['token', 'payload'])
DEFAULT_APNS_PRIORITY = NotificationPriority.Immediate DEFAULT_APNS_PRIORITY = NotificationPriority.Immediate
@@ -47,15 +48,13 @@ class APNsClient(object):
ALTERNATIVE_PORT = 2197 ALTERNATIVE_PORT = 2197
def __init__(self, def __init__(self,
credentials: Union[Credentials, str], credentials: TokenCredentials,
use_sandbox: bool = False, use_alternative_port: bool = False, proto: Optional[str] = None, use_sandbox: bool = False, use_alternative_port: bool = False, proto: Optional[str] = None,
json_encoder: Optional[type] = None, password: Optional[str] = None, json_encoder: Optional[type] = None, password: Optional[str] = None,
proxy_host: Optional[str] = None, proxy_port: Optional[int] = None, proxy_host: Optional[str] = None, proxy_port: Optional[int] = None,
heartbeat_period: Optional[float] = None) -> None: heartbeat_period: Optional[float] = None) -> None:
if isinstance(credentials, str):
self.__credentials = CertificateCredentials(credentials, password) # type: Credentials self.__credentials = credentials
else:
self.__credentials = credentials
self._init_connection(use_sandbox, use_alternative_port, proto, proxy_host, proxy_port) self._init_connection(use_sandbox, use_alternative_port, proto, proxy_host, proxy_port)
if heartbeat_period: if heartbeat_period:
@@ -67,9 +66,9 @@ class APNsClient(object):
def _init_connection(self, use_sandbox: bool, use_alternative_port: bool, proto: Optional[str], def _init_connection(self, use_sandbox: bool, use_alternative_port: bool, proto: Optional[str],
proxy_host: Optional[str], proxy_port: Optional[int]) -> None: proxy_host: Optional[str], proxy_port: Optional[int]) -> None:
server = self.SANDBOX_SERVER if use_sandbox else self.LIVE_SERVER self.__server = self.SANDBOX_SERVER if use_sandbox else self.LIVE_SERVER
port = self.ALTERNATIVE_PORT if use_alternative_port else self.DEFAULT_PORT self.__port = self.ALTERNATIVE_PORT if use_alternative_port else self.DEFAULT_PORT
self._connection = self.__credentials.create_connection(server, port, proto, proxy_host, proxy_port) self.__client = httpx.Client(http2=True)
def _start_heartbeat(self, heartbeat_period: float) -> None: def _start_heartbeat(self, heartbeat_period: float) -> None:
conn_ref = weakref.ref(self._connection) conn_ref = weakref.ref(self._connection)
@@ -142,24 +141,25 @@ class APNsClient(object):
headers['apns-collapse-id'] = collapse_id headers['apns-collapse-id'] = collapse_id
url = '/3/device/{}'.format(token_hex) url = '/3/device/{}'.format(token_hex)
stream_id = self._connection.request('POST', url, json_payload, headers) # type: int response = self.__client.post('https://{}{}'.format(self.__server, url), headers=headers, data=json_payload)
return stream_id return response.status_code, response.text
def get_notification_result(self, stream_id: int) -> Union[str, Tuple[str, str]]: def get_notification_result(self, status: int, reason: str) -> Union[str, Tuple[str, str]]:
""" """
Get result for specified stream Get result for specified stream
The function returns: 'Success' or 'failure reason' or ('Unregistered', timestamp) The function returns: 'Success' or 'failure reason' or ('Unregistered', timestamp)
""" """
with self._connection.get_response(stream_id) as response: # with self._connection.get_response(stream_id) as response:
if response.status == 200: if status == 200:
return 'Success' return 'Success'
else: else:
raw_data = response.read().decode('utf-8') return reason
data = json.loads(raw_data) # type: Dict[str, str] # raw_data = response.read().decode('utf-8')
if response.status == 410: # data = json.loads(raw_data) # type: Dict[str, str]
return data['reason'], data['timestamp'] # if response.status == 410:
else: # return data['reason'], data['timestamp']
return data['reason'] # else:
# return data['reason']
def send_notification_batch(self, notifications: Iterable[Notification], topic: Optional[str] = None, def send_notification_batch(self, notifications: Iterable[Notification], topic: Optional[str] = None,
priority: NotificationPriority = NotificationPriority.Immediate, priority: NotificationPriority = NotificationPriority.Immediate,
@@ -182,7 +182,7 @@ class APNsClient(object):
next_notification = next(notification_iterator, None) next_notification = next(notification_iterator, None)
# Make sure we're connected to APNs, so that we receive and process the server's SETTINGS # Make sure we're connected to APNs, so that we receive and process the server's SETTINGS
# frame before starting to send notifications. # frame before starting to send notifications.
self.connect() # self.connect()
results = {} results = {}
open_streams = collections.deque() # type: typing.Deque[RequestStream] open_streams = collections.deque() # type: typing.Deque[RequestStream]
@@ -195,9 +195,9 @@ class APNsClient(object):
self.update_max_concurrent_streams() self.update_max_concurrent_streams()
if next_notification is not None and len(open_streams) < self.__max_concurrent_streams: if next_notification is not None and len(open_streams) < self.__max_concurrent_streams:
logger.info('Sending to token %s', next_notification.token) logger.info('Sending to token %s', next_notification.token)
stream_id = self.send_notification_async(next_notification.token, next_notification.payload, topic, status, reason = self.send_notification_async(next_notification.token, next_notification.payload, topic,
priority, expiration, collapse_id, push_type) priority, expiration, collapse_id, push_type)
open_streams.append(RequestStream(stream_id, next_notification.token)) open_streams.append(RequestStream(next_notification.token, status, reason))
next_notification = next(notification_iterator, None) next_notification = next(notification_iterator, None)
if next_notification is None: if next_notification is None:
@@ -208,7 +208,7 @@ class APNsClient(object):
# sent new requests or exited the while loop.) Wait for the first outstanding stream # sent new requests or exited the while loop.) Wait for the first outstanding stream
# to return a response. # to return a response.
pending_stream = open_streams.popleft() pending_stream = open_streams.popleft()
result = self.get_notification_result(pending_stream.stream_id) result = self.get_notification_result(pending_stream.status, pending_stream.reason)
logger.info('Got response for %s: %s', pending_stream.token, result) logger.info('Got response for %s: %s', pending_stream.token, result)
results[pending_stream.token] = result results[pending_stream.token] = result
@@ -216,11 +216,7 @@ class APNsClient(object):
def update_max_concurrent_streams(self) -> None: def update_max_concurrent_streams(self) -> None:
# Get the max_concurrent_streams setting returned by the server. # Get the max_concurrent_streams setting returned by the server.
# The max_concurrent_streams value is saved in the H2Connection instance that must be max_concurrent_streams = 100
# accessed using a with statement in order to acquire a lock.
# pylint: disable=protected-access
with self._connection._conn as connection:
max_concurrent_streams = connection.remote_settings.max_concurrent_streams
if max_concurrent_streams == self.__previous_server_max_concurrent_streams: if max_concurrent_streams == self.__previous_server_max_concurrent_streams:
# The server hasn't issued an updated SETTINGS frame. # The server hasn't issued an updated SETTINGS frame.
+1 -25
View File
@@ -3,43 +3,19 @@ from typing import Optional, Tuple, TYPE_CHECKING
import jwt import jwt
from hyper import HTTP20Connection # type: ignore
from hyper.tls import init_context # type: ignore
if TYPE_CHECKING:
from hyper.ssl_compat import SSLContext # type: ignore
DEFAULT_TOKEN_LIFETIME = 3600 DEFAULT_TOKEN_LIFETIME = 3600
DEFAULT_TOKEN_ENCRYPTION_ALGORITHM = 'ES256' DEFAULT_TOKEN_ENCRYPTION_ALGORITHM = 'ES256'
# Abstract Base class. This should not be instantiated directly. # Abstract Base class. This should not be instantiated directly.
class Credentials(object): class Credentials(object):
def __init__(self, ssl_context: 'Optional[SSLContext]' = None) -> None: def __init__(self):
super().__init__() super().__init__()
self.__ssl_context = ssl_context
# Creates a connection with the credentials, if available or necessary.
def create_connection(self, server: str, port: int, proto: Optional[str], proxy_host: Optional[str] = None,
proxy_port: Optional[int] = None) -> HTTP20Connection:
# self.__ssl_context may be none, and that's fine.
return HTTP20Connection(server, port, ssl_context=self.__ssl_context, force_proto=proto or 'h2',
secure=True, proxy_host=proxy_host, proxy_port=proxy_port)
def get_authorization_header(self, topic: Optional[str]) -> Optional[str]: def get_authorization_header(self, topic: Optional[str]) -> Optional[str]:
return None return None
# Credentials subclass for certificate authentication
class CertificateCredentials(Credentials):
def __init__(self, cert_file: Optional[str] = None, password: Optional[str] = None,
cert_chain: Optional[str] = None) -> None:
ssl_context = init_context(cert=cert_file, cert_password=password)
if cert_chain:
ssl_context.load_cert_chain(cert_chain)
super(CertificateCredentials, self).__init__(ssl_context)
# Credentials subclass for JWT token based authentication # Credentials subclass for JWT token based authentication
class TokenCredentials(Credentials): class TokenCredentials(Credentials):
def __init__(self, auth_key_path: str, auth_key_id: str, team_id: str, def __init__(self, auth_key_path: str, auth_key_id: str, team_id: str,
+2 -2
View File
@@ -4,10 +4,10 @@ from setuptools import setup
setup( setup(
name='apns2', name='apns2',
version='0.7.1', version='0.7.2',
packages=['apns2'], packages=['apns2'],
install_requires=[ install_requires=[
'hyper>=0.7', 'httpx>=0.13.0',
'PyJWT>=1.4.0', 'PyJWT>=1.4.0',
'cryptography>=1.7.2', 'cryptography>=1.7.2',
], ],