remove hyper from apns2
This commit is contained in:
+29
-33
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user