{"action":"created","comment":{"url":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/comments/60839991","id":60839991,"diff_hunk":"@@ -1,39 +1,149 @@\n+import collections\n+import json\n+import logging\n from enum import Enum\n-from json import dumps\n \n-import json\n from hyper import HTTP20Connection\n from hyper.tls import init_context\n \n-from apns2.errors import exception_class_for_reason\n-\n+from .errors import exception_class_for_reason\n \n class NotificationPriority(Enum):\n-    Immediate = 10\n-    Delayed = 5\n+    Immediate = '10'\n+    Delayed = '5'\n \n+DEFAULT_APNS_PRIORITY = NotificationPriority.Immediate\n+CONCURRENT_STREAMS_SAFETY_MAXIMUM = 1000\n+\n+logger = logging.getLogger(__name__)\n \n class APNsClient(object):\n     def __init__(self, cert_file, use_sandbox=False, use_alternative_port=False):\n         server = 'api.development.push.apple.com' if use_sandbox else 'api.push.apple.com'\n         port = 2197 if use_alternative_port else 443\n         ssl_context = init_context()\n         ssl_context.load_cert_chain(cert_file)\n-        self.__connection = HTTP20Connection(server, port, ssl_context=ssl_context)\n+        self._connection = HTTP20Connection(server, port, ssl_context=ssl_context)\n+        self._max_concurrent_streams = None\n+        self._previous_server_max_concurrent_streams = None\n+\n+    def send_notification(\n+        self,\n+        token_hex,\n+        notification,\n+        topic,\n+        priority=NotificationPriority.Immediate\n+    ):\n+        stream_id = self.send_notification_async(token_hex, notification, topic, priority)\n+        result = self.get_notification_result(stream_id)\n+        if result != 'Success':\n+            raise exception_class_for_reason(result)\n \n-    def send_notification(self, token_hex, notification, priority=NotificationPriority.Immediate, topic=None):\n-        json_payload = dumps(notification.dict(), ensure_ascii=False, separators=(',', ':')).encode('utf-8')\n+    def send_notification_async(\n+        self,\n+        token_hex,\n+        notification,\n+        topic,\n+        priority=NotificationPriority.Immediate\n+    ):\n+        json_payload = json.dumps(\n+            notification.dict(),\n+            ensure_ascii=False,\n+            separators=(',', ':')\n+        ).encode('utf-8')\n \n-        headers = {\n-            'apns-priority': priority.value\n-        }\n-        if topic:\n-            headers['apns-topic'] = topic\n+        headers = {'apns-topic': topic}\n+        if priority != DEFAULT_APNS_PRIORITY:\n+            headers['apns-priority'] = priority.value\n \n         url = '/3/device/{}'.format(token_hex)\n-        stream_id = self.__connection.request('POST', url, json_payload, headers)\n-        resp = self.__connection.get_response(stream_id)\n-        if resp.status != 200:\n-            raw_data = resp.read().decode('utf-8')\n+        stream_id = self._connection.request('POST', url, json_payload, headers)\n+        return stream_id\n+\n+    def get_notification_result(self, stream_id):\n+        response = self._connection.get_response(stream_id)\n+        if response.status == 200:\n+            return 'Success'\n+        else:\n+            raw_data = response.read().decode('utf-8')\n             data = json.loads(raw_data)\n-            raise exception_class_for_reason(data['reason'])\n+            return data['reason']\n+\n+    def send_notification_batch(\n+        self,\n+        tokens,\n+        notification,\n+        topic,\n+        priority=NotificationPriority.Immediate\n+    ):\n+        '''\n+        Send a notification to a list of tokens in batch. Instead of sending a synchronous request\n+        for each token, send multiple requests concurrently. This is done on the same connection,\n+        using HTTP/2 streams (one request per stream).\n+\n+        APNs allows many streams simultaneously, but the number of streams can vary depending on\n+        server load. This method reads the SETTINGS frame sent by the server to figure out the\n+        maximum number of concurrent streams. Typically, APNs reports a maximum of 500.\n+        '''\n+        # Make sure we're connected to APNs, so that we receive and process the server's SETTINGS\n+        # frame before starting to send notifications.\n+        self._connection.connect()\n+        result_counter = collections.Counter()\n+        open_streams = collections.deque()\n+        token_iterator = iter(tokens)\n+        next_token = next(token_iterator)\n+        # Loop on the tokens, sending as many requests as possible concurrently to APNs.\n+        # When reaching the maximum concurrent streams limit, wait for a response before sending\n+        # another request.\n+        while open_streams or next_token:","path":"apns2/client.py","position":117,"original_position":117,"commit_id":"a2e0ac4447080883715ec89c1aa10f9cc6b3626e","original_commit_id":"a2e0ac4447080883715ec89c1aa10f9cc6b3626e","user":{"login":"mccomplete","id":7974749,"avatar_url":"https://avatars.githubusercontent.com/u/7974749?v=3","gravatar_id":"","url":"https://api.github.com/users/mccomplete","html_url":"https://github.com/mccomplete","followers_url":"https://api.github.com/users/mccomplete/followers","following_url":"https://api.github.com/users/mccomplete/following{/other_user}","gists_url":"https://api.github.com/users/mccomplete/gists{/gist_id}","starred_url":"https://api.github.com/users/mccomplete/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mccomplete/subscriptions","organizations_url":"https://api.github.com/users/mccomplete/orgs","repos_url":"https://api.github.com/users/mccomplete/repos","events_url":"https://api.github.com/users/mccomplete/events{/privacy}","received_events_url":"https://api.github.com/users/mccomplete/received_events","type":"User","site_admin":false},"body":"I usually prefer not to rely on empty containers being falsy.  I think\r\n\r\n```python\r\nwhile len(open_streams) > 0 or next_token is not None\r\n```\r\n\r\nis much more readable.  By the way, how do you know that next() won't raise StopIteration?","created_at":"2016-04-24T06:59:35Z","updated_at":"2016-04-24T06:59:35Z","html_url":"https://github.com/Lightricks/PyAPNs2/pull/1#discussion_r60839991","pull_request_url":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/1","_links":{"self":{"href":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/comments/60839991"},"html":{"href":"https://github.com/Lightricks/PyAPNs2/pull/1#discussion_r60839991"},"pull_request":{"href":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/1"}}},"pull_request":{"url":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/1","id":67211544,"html_url":"https://github.com/Lightricks/PyAPNs2/pull/1","diff_url":"https://github.com/Lightricks/PyAPNs2/pull/1.diff","patch_url":"https://github.com/Lightricks/PyAPNs2/pull/1.patch","issue_url":"https://api.github.com/repos/Lightricks/PyAPNs2/issues/1","number":1,"state":"open","locked":false,"title":"[feature/batch_send] Add send_notification_batch","user":{"login":"matangover","id":5028956,"avatar_url":"https://avatars.githubusercontent.com/u/5028956?v=3","gravatar_id":"","url":"https://api.github.com/users/matangover","html_url":"https://github.com/matangover","followers_url":"https://api.github.com/users/matangover/followers","following_url":"https://api.github.com/users/matangover/following{/other_user}","gists_url":"https://api.github.com/users/matangover/gists{/gist_id}","starred_url":"https://api.github.com/users/matangover/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/matangover/subscriptions","organizations_url":"https://api.github.com/users/matangover/orgs","repos_url":"https://api.github.com/users/matangover/repos","events_url":"https://api.github.com/users/matangover/events{/privacy}","received_events_url":"https://api.github.com/users/matangover/received_events","type":"User","site_admin":false},"body":"","created_at":"2016-04-20T15:49:18Z","updated_at":"2016-04-24T06:59:35Z","closed_at":null,"merged_at":null,"merge_commit_sha":"5789281ce5864f14dc6f531faeb2a130b415003a","assignee":{"login":"mccomplete","id":7974749,"avatar_url":"https://avatars.githubusercontent.com/u/7974749?v=3","gravatar_id":"","url":"https://api.github.com/users/mccomplete","html_url":"https://github.com/mccomplete","followers_url":"https://api.github.com/users/mccomplete/followers","following_url":"https://api.github.com/users/mccomplete/following{/other_user}","gists_url":"https://api.github.com/users/mccomplete/gists{/gist_id}","starred_url":"https://api.github.com/users/mccomplete/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mccomplete/subscriptions","organizations_url":"https://api.github.com/users/mccomplete/orgs","repos_url":"https://api.github.com/users/mccomplete/repos","events_url":"https://api.github.com/users/mccomplete/events{/privacy}","received_events_url":"https://api.github.com/users/mccomplete/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/1/commits","review_comments_url":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/1/comments","review_comment_url":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Lightricks/PyAPNs2/issues/1/comments","statuses_url":"https://api.github.com/repos/Lightricks/PyAPNs2/statuses/a2e0ac4447080883715ec89c1aa10f9cc6b3626e","head":{"label":"Lightricks:feature/batch_send","ref":"feature/batch_send","sha":"a2e0ac4447080883715ec89c1aa10f9cc6b3626e","user":{"login":"Lightricks","id":3170348,"avatar_url":"https://avatars.githubusercontent.com/u/3170348?v=3","gravatar_id":"","url":"https://api.github.com/users/Lightricks","html_url":"https://github.com/Lightricks","followers_url":"https://api.github.com/users/Lightricks/followers","following_url":"https://api.github.com/users/Lightricks/following{/other_user}","gists_url":"https://api.github.com/users/Lightricks/gists{/gist_id}","starred_url":"https://api.github.com/users/Lightricks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Lightricks/subscriptions","organizations_url":"https://api.github.com/users/Lightricks/orgs","repos_url":"https://api.github.com/users/Lightricks/repos","events_url":"https://api.github.com/users/Lightricks/events{/privacy}","received_events_url":"https://api.github.com/users/Lightricks/received_events","type":"Organization","site_admin":false},"repo":{"id":54821142,"name":"PyAPNs2","full_name":"Lightricks/PyAPNs2","owner":{"login":"Lightricks","id":3170348,"avatar_url":"https://avatars.githubusercontent.com/u/3170348?v=3","gravatar_id":"","url":"https://api.github.com/users/Lightricks","html_url":"https://github.com/Lightricks","followers_url":"https://api.github.com/users/Lightricks/followers","following_url":"https://api.github.com/users/Lightricks/following{/other_user}","gists_url":"https://api.github.com/users/Lightricks/gists{/gist_id}","starred_url":"https://api.github.com/users/Lightricks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Lightricks/subscriptions","organizations_url":"https://api.github.com/users/Lightricks/orgs","repos_url":"https://api.github.com/users/Lightricks/repos","events_url":"https://api.github.com/users/Lightricks/events{/privacy}","received_events_url":"https://api.github.com/users/Lightricks/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Lightricks/PyAPNs2","description":"Python library for interacting with the Apple Push Notification service (APNs) via HTTP/2 protocol","fork":true,"url":"https://api.github.com/repos/Lightricks/PyAPNs2","forks_url":"https://api.github.com/repos/Lightricks/PyAPNs2/forks","keys_url":"https://api.github.com/repos/Lightricks/PyAPNs2/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Lightricks/PyAPNs2/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Lightricks/PyAPNs2/teams","hooks_url":"https://api.github.com/repos/Lightricks/PyAPNs2/hooks","issue_events_url":"https://api.github.com/repos/Lightricks/PyAPNs2/issues/events{/number}","events_url":"https://api.github.com/repos/Lightricks/PyAPNs2/events","assignees_url":"https://api.github.com/repos/Lightricks/PyAPNs2/assignees{/user}","branches_url":"https://api.github.com/repos/Lightricks/PyAPNs2/branches{/branch}","tags_url":"https://api.github.com/repos/Lightricks/PyAPNs2/tags","blobs_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/refs{/sha}","trees_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Lightricks/PyAPNs2/statuses/{sha}","languages_url":"https://api.github.com/repos/Lightricks/PyAPNs2/languages","stargazers_url":"https://api.github.com/repos/Lightricks/PyAPNs2/stargazers","contributors_url":"https://api.github.com/repos/Lightricks/PyAPNs2/contributors","subscribers_url":"https://api.github.com/repos/Lightricks/PyAPNs2/subscribers","subscription_url":"https://api.github.com/repos/Lightricks/PyAPNs2/subscription","commits_url":"https://api.github.com/repos/Lightricks/PyAPNs2/commits{/sha}","git_commits_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/commits{/sha}","comments_url":"https://api.github.com/repos/Lightricks/PyAPNs2/comments{/number}","issue_comment_url":"https://api.github.com/repos/Lightricks/PyAPNs2/issues/comments{/number}","contents_url":"https://api.github.com/repos/Lightricks/PyAPNs2/contents/{+path}","compare_url":"https://api.github.com/repos/Lightricks/PyAPNs2/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Lightricks/PyAPNs2/merges","archive_url":"https://api.github.com/repos/Lightricks/PyAPNs2/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Lightricks/PyAPNs2/downloads","issues_url":"https://api.github.com/repos/Lightricks/PyAPNs2/issues{/number}","pulls_url":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls{/number}","milestones_url":"https://api.github.com/repos/Lightricks/PyAPNs2/milestones{/number}","notifications_url":"https://api.github.com/repos/Lightricks/PyAPNs2/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Lightricks/PyAPNs2/labels{/name}","releases_url":"https://api.github.com/repos/Lightricks/PyAPNs2/releases{/id}","deployments_url":"https://api.github.com/repos/Lightricks/PyAPNs2/deployments","created_at":"2016-03-27T09:20:15Z","updated_at":"2016-03-27T09:20:16Z","pushed_at":"2016-04-21T11:58:45Z","git_url":"git://github.com/Lightricks/PyAPNs2.git","ssh_url":"git@github.com:Lightricks/PyAPNs2.git","clone_url":"https://github.com/Lightricks/PyAPNs2.git","svn_url":"https://github.com/Lightricks/PyAPNs2","homepage":"","size":38,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"Lightricks:master","ref":"master","sha":"41ed1590241109a9fe6b65a0cec11d558cbc2046","user":{"login":"Lightricks","id":3170348,"avatar_url":"https://avatars.githubusercontent.com/u/3170348?v=3","gravatar_id":"","url":"https://api.github.com/users/Lightricks","html_url":"https://github.com/Lightricks","followers_url":"https://api.github.com/users/Lightricks/followers","following_url":"https://api.github.com/users/Lightricks/following{/other_user}","gists_url":"https://api.github.com/users/Lightricks/gists{/gist_id}","starred_url":"https://api.github.com/users/Lightricks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Lightricks/subscriptions","organizations_url":"https://api.github.com/users/Lightricks/orgs","repos_url":"https://api.github.com/users/Lightricks/repos","events_url":"https://api.github.com/users/Lightricks/events{/privacy}","received_events_url":"https://api.github.com/users/Lightricks/received_events","type":"Organization","site_admin":false},"repo":{"id":54821142,"name":"PyAPNs2","full_name":"Lightricks/PyAPNs2","owner":{"login":"Lightricks","id":3170348,"avatar_url":"https://avatars.githubusercontent.com/u/3170348?v=3","gravatar_id":"","url":"https://api.github.com/users/Lightricks","html_url":"https://github.com/Lightricks","followers_url":"https://api.github.com/users/Lightricks/followers","following_url":"https://api.github.com/users/Lightricks/following{/other_user}","gists_url":"https://api.github.com/users/Lightricks/gists{/gist_id}","starred_url":"https://api.github.com/users/Lightricks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Lightricks/subscriptions","organizations_url":"https://api.github.com/users/Lightricks/orgs","repos_url":"https://api.github.com/users/Lightricks/repos","events_url":"https://api.github.com/users/Lightricks/events{/privacy}","received_events_url":"https://api.github.com/users/Lightricks/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Lightricks/PyAPNs2","description":"Python library for interacting with the Apple Push Notification service (APNs) via HTTP/2 protocol","fork":true,"url":"https://api.github.com/repos/Lightricks/PyAPNs2","forks_url":"https://api.github.com/repos/Lightricks/PyAPNs2/forks","keys_url":"https://api.github.com/repos/Lightricks/PyAPNs2/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Lightricks/PyAPNs2/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Lightricks/PyAPNs2/teams","hooks_url":"https://api.github.com/repos/Lightricks/PyAPNs2/hooks","issue_events_url":"https://api.github.com/repos/Lightricks/PyAPNs2/issues/events{/number}","events_url":"https://api.github.com/repos/Lightricks/PyAPNs2/events","assignees_url":"https://api.github.com/repos/Lightricks/PyAPNs2/assignees{/user}","branches_url":"https://api.github.com/repos/Lightricks/PyAPNs2/branches{/branch}","tags_url":"https://api.github.com/repos/Lightricks/PyAPNs2/tags","blobs_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/refs{/sha}","trees_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Lightricks/PyAPNs2/statuses/{sha}","languages_url":"https://api.github.com/repos/Lightricks/PyAPNs2/languages","stargazers_url":"https://api.github.com/repos/Lightricks/PyAPNs2/stargazers","contributors_url":"https://api.github.com/repos/Lightricks/PyAPNs2/contributors","subscribers_url":"https://api.github.com/repos/Lightricks/PyAPNs2/subscribers","subscription_url":"https://api.github.com/repos/Lightricks/PyAPNs2/subscription","commits_url":"https://api.github.com/repos/Lightricks/PyAPNs2/commits{/sha}","git_commits_url":"https://api.github.com/repos/Lightricks/PyAPNs2/git/commits{/sha}","comments_url":"https://api.github.com/repos/Lightricks/PyAPNs2/comments{/number}","issue_comment_url":"https://api.github.com/repos/Lightricks/PyAPNs2/issues/comments{/number}","contents_url":"https://api.github.com/repos/Lightricks/PyAPNs2/contents/{+path}","compare_url":"https://api.github.com/repos/Lightricks/PyAPNs2/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Lightricks/PyAPNs2/merges","archive_url":"https://api.github.com/repos/Lightricks/PyAPNs2/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Lightricks/PyAPNs2/downloads","issues_url":"https://api.github.com/repos/Lightricks/PyAPNs2/issues{/number}","pulls_url":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls{/number}","milestones_url":"https://api.github.com/repos/Lightricks/PyAPNs2/milestones{/number}","notifications_url":"https://api.github.com/repos/Lightricks/PyAPNs2/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Lightricks/PyAPNs2/labels{/name}","releases_url":"https://api.github.com/repos/Lightricks/PyAPNs2/releases{/id}","deployments_url":"https://api.github.com/repos/Lightricks/PyAPNs2/deployments","created_at":"2016-03-27T09:20:15Z","updated_at":"2016-03-27T09:20:16Z","pushed_at":"2016-04-21T11:58:45Z","git_url":"git://github.com/Lightricks/PyAPNs2.git","ssh_url":"git@github.com:Lightricks/PyAPNs2.git","clone_url":"https://github.com/Lightricks/PyAPNs2.git","svn_url":"https://github.com/Lightricks/PyAPNs2","homepage":"","size":38,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/1"},"html":{"href":"https://github.com/Lightricks/PyAPNs2/pull/1"},"issue":{"href":"https://api.github.com/repos/Lightricks/PyAPNs2/issues/1"},"comments":{"href":"https://api.github.com/repos/Lightricks/PyAPNs2/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Lightricks/PyAPNs2/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/Lightricks/PyAPNs2/statuses/a2e0ac4447080883715ec89c1aa10f9cc6b3626e"}}}}