Full Code of Fatal1ty/aioapns for AI

master 96831003ec5a cached
17 files
46.8 KB
10.5k tokens
79 symbols
1 requests
Download .txt
Repository: Fatal1ty/aioapns
Branch: master
Commit: 96831003ec5a
Files: 17
Total size: 46.8 KB

Directory structure:
gitextract_rn18lmov/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── LICENSE
├── README.rst
├── aioapns/
│   ├── __init__.py
│   ├── client.py
│   ├── common.py
│   ├── connection.py
│   ├── exceptions.py
│   ├── logging.py
│   └── py.typed
├── examples/
│   └── client.py
├── pyproject.toml
├── requirements-dev.txt
├── setup.cfg
└── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
custom: ['https://coindrop.to/tikhonov_a']


================================================
FILE: .github/workflows/ci.yml
================================================
name: tests

on:
  push:
    branches:
      - '*'
  pull_request:
    branches:
      - master

jobs:
  tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.8", "3.9", "3.10", "3.11"]

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}
        cache: pip
    - run: pip install -r requirements-dev.txt
    - name: Link with flake8
      run: flake8 aioapns
    - name: Run mypy
      run: mypy aioapns
    - name: Run black
      run: black --check .


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# PyCharm
.idea

================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2017 Alexander Tikhonov

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.rst
================================================
aioapns - An efficient APNs Client Library for Python/asyncio
=================================================================================

.. image:: https://github.com/Fatal1ty/aioapns/workflows/tests/badge.svg
   :target: https://github.com/Fatal1ty/aioapns/actions

.. image:: https://img.shields.io/pypi/v/aioapns.svg
    :target: https://pypi.python.org/pypi/aioapns

.. image:: https://img.shields.io/pypi/pyversions/aioapns.svg
    :target: https://pypi.python.org/pypi/aioapns/

.. image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg
    :target: https://opensource.org/licenses/Apache-2.0

**aioapns** is a library designed specifically for sending push-notifications to iOS devices
via Apple Push Notification Service. aioapns provides an efficient client through
asynchronous HTTP2 protocol for use with Python's ``asyncio``
framework.

aioapns requires Python 3.8 or later.


Performance
-----------

In my testing aioapns allows you to send on average 1.3k notifications per second on a single core.


Features
--------

* Internal connection pool which adapts to the current load
* Support for certificate and token based connections
* Ability to set TTL (time to live) for notifications
* Ability to set priority for notifications
* Ability to set collapse-key for notifications
* Ability to use production or development APNs server
* Support for basic HTTP-Proxies


Installation
------------

Use pip to install::

    $ pip install aioapns


Basic Usage
-----------

.. code-block:: python

    import asyncio
    from uuid import uuid4
    from aioapns import APNs, NotificationRequest, PushType


    async def run():
        apns_cert_client = APNs(
            client_cert='/path/to/apns-cert.pem',
            use_sandbox=False,
        )
        with read('/path/to/apns-key.p8') as key:
            apns_key_client = APNs(
                key=key,
                key_id='<KEY_ID>',
                team_id='<TEAM_ID>',
                topic='<APNS_TOPIC>',  # Bundle ID
                use_sandbox=False,
            )
        request = NotificationRequest(
            device_token='<DEVICE_TOKEN>',
            message = {
                "aps": {
                    "alert": "Hello from APNs",
                    "badge": "1",
                }
            },
            notification_id=str(uuid4()),  # optional
            time_to_live=3,                # optional
            push_type=PushType.ALERT,      # optional
        )
        await apns_cert_client.send_notification(request)
        await apns_key_client.send_notification(request)

    loop = asyncio.get_event_loop()
    loop.run_until_complete(run())


License
-------

aioapns is developed and distributed under the Apache 2.0 license.


================================================
FILE: aioapns/__init__.py
================================================
from aioapns.client import APNs
from aioapns.common import (
    PRIORITY_HIGH,
    PRIORITY_NORMAL,
    NotificationRequest,
    PushType,
)
from aioapns.exceptions import ConnectionError

__all__ = (
    "APNs",
    "NotificationRequest",
    "PRIORITY_NORMAL",
    "PRIORITY_HIGH",
    "ConnectionError",
    "PushType",
)


================================================
FILE: aioapns/client.py
================================================
from ssl import SSLContext
from typing import Awaitable, Callable, Optional

from aioapns.common import NotificationRequest, NotificationResult
from aioapns.connection import (
    APNsBaseConnectionPool,
    APNsCertConnectionPool,
    APNsKeyConnectionPool,
)
from aioapns.logging import logger


class APNs:
    def __init__(
        self,
        client_cert: Optional[str] = None,
        key: Optional[str] = None,
        key_id: Optional[str] = None,
        team_id: Optional[str] = None,
        topic: Optional[str] = None,
        max_connections: int = 10,
        max_connection_attempts: int = 5,
        use_sandbox: bool = False,
        no_cert_validation: bool = False,
        ssl_context: Optional[SSLContext] = None,
        proxy_host: Optional[str] = None,
        proxy_port: Optional[int] = None,
        err_func: Optional[
            Callable[
                [NotificationRequest, NotificationResult], Awaitable[None]
            ]
        ] = None,
    ) -> None:
        self.pool: APNsBaseConnectionPool
        self.err_func = err_func
        if client_cert is not None and key is not None:
            raise ValueError("cannot specify both client_cert and key")
        elif client_cert:
            self.pool = APNsCertConnectionPool(
                cert_file=client_cert,
                topic=topic,
                max_connections=max_connections,
                max_connection_attempts=max_connection_attempts,
                use_sandbox=use_sandbox,
                no_cert_validation=no_cert_validation,
                ssl_context=ssl_context,
                proxy_host=proxy_host,
                proxy_port=proxy_port,
            )
        elif key and key_id and team_id and topic:
            self.pool = APNsKeyConnectionPool(
                key=key,
                key_id=key_id,
                team_id=team_id,
                topic=topic,
                max_connections=max_connections,
                max_connection_attempts=max_connection_attempts,
                use_sandbox=use_sandbox,
                ssl_context=ssl_context,
                proxy_host=proxy_host,
                proxy_port=proxy_port,
            )
        else:
            raise ValueError(
                "You must provide either APNs cert file path or "
                "the key credentials"
            )

    async def send_notification(
        self, request: NotificationRequest
    ) -> NotificationResult:
        response = await self.pool.send_notification(request)
        if not response.is_successful:
            if self.err_func is not None:
                await self.err_func(request, response)
            else:
                logger.error(
                    "Status of notification %s is %s (%s)",
                    request.notification_id,
                    response.status,
                    response.description,
                )
        return response


================================================
FILE: aioapns/common.py
================================================
import asyncio
from enum import Enum
from typing import Any, Dict, Optional
from uuid import uuid4

PRIORITY_NORMAL = "5"
PRIORITY_HIGH = "10"


class PushType(Enum):
    ALERT = "alert"
    BACKGROUND = "background"
    VOIP = "voip"
    COMPLICATION = "complication"
    FILEPROVIDER = "fileprovider"
    MDM = "mdm"
    LIVEACTIVITY = "liveactivity"


class NotificationRequest:
    __slots__ = (
        "device_token",
        "message",
        "notification_id",
        "time_to_live",
        "priority",
        "collapse_key",
        "push_type",
        "apns_topic",
    )

    def __init__(
        self,
        device_token: str,
        message: Dict[str, Any],
        notification_id: Optional[str] = None,
        time_to_live: Optional[int] = None,
        priority: Optional[int] = None,
        collapse_key: Optional[str] = None,
        push_type: Optional[PushType] = None,
        *,
        apns_topic: Optional[str] = None,
    ) -> None:
        self.device_token = device_token
        self.message = message
        self.notification_id = notification_id or str(uuid4())
        self.time_to_live = time_to_live
        self.priority = priority
        self.collapse_key = collapse_key
        self.push_type = push_type
        self.apns_topic = apns_topic


class NotificationResult:
    __slots__ = ("notification_id", "status", "description", "timestamp")

    def __init__(
        self,
        notification_id: str,
        status: str,
        description: Optional[str] = None,
        timestamp: Optional[int] = None,
    ):
        self.notification_id = notification_id
        self.status = status
        self.description = description
        self.timestamp = timestamp

    @property
    def is_successful(self) -> bool:
        return self.status == APNS_RESPONSE_CODE.SUCCESS


class DynamicBoundedSemaphore(asyncio.BoundedSemaphore):
    _bound_value: int

    @property
    def bound(self) -> int:
        return self._bound_value

    @bound.setter
    def bound(self, new_bound: int) -> None:
        if new_bound > self._bound_value:
            if self._value > 0:
                self._value += new_bound - self._bound_value
            if self._value <= 0:
                for _ in range(new_bound - self._bound_value):
                    self.release()
        elif new_bound < self._bound_value:
            self._value -= self._bound_value - new_bound
        self._bound_value = new_bound

    def release(self) -> None:
        self._value += 1
        if self._value > self._bound_value:
            self._value = self._bound_value
        self._wake_up_next()

    def destroy(self, exc: Exception) -> None:
        while self._waiters:
            waiter = self._waiters.popleft()
            if not waiter.done():
                waiter.set_exception(exc)


class APNS_RESPONSE_CODE:
    SUCCESS = "200"
    BAD_REQUEST = "400"
    FORBIDDEN = "403"
    METHOD_NOT_ALLOWED = "405"
    GONE = "410"
    PAYLOAD_TOO_LARGE = "413"
    TOO_MANY_REQUESTS = "429"
    INTERNAL_SERVER_ERROR = "500"
    SERVICE_UNAVAILABLE = "503"


================================================
FILE: aioapns/connection.py
================================================
import asyncio
import json
import ssl
import time
from functools import partial
from typing import Any, Callable, Dict, List, NoReturn, Optional, Type

import jwt
import OpenSSL
from h2.connection import H2Connection
from h2.events import (
    ConnectionTerminated,
    DataReceived,
    RemoteSettingsChanged,
    ResponseReceived,
    SettingsAcknowledged,
    StreamEnded,
    WindowUpdated,
)
from h2.exceptions import FlowControlError, NoAvailableStreamIDError
from h2.settings import ChangedSetting, SettingCodes

from aioapns.common import (
    APNS_RESPONSE_CODE,
    DynamicBoundedSemaphore,
    NotificationRequest,
    NotificationResult,
)
from aioapns.exceptions import (
    ConnectionClosed,
    ConnectionError,
    MaxAttemptsExceeded,
)
from aioapns.logging import logger


class ChannelPool(DynamicBoundedSemaphore):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super(ChannelPool, self).__init__(*args, **kwargs)
        self._stream_id = -1

    async def acquire(self) -> int:  # type: ignore
        await super(ChannelPool, self).acquire()
        self._stream_id += 2
        if self._stream_id > H2Connection.HIGHEST_ALLOWED_STREAM_ID:
            raise NoAvailableStreamIDError()
        return self._stream_id

    @property
    def is_busy(self) -> bool:
        return self._value <= 0


class AuthorizationHeaderProvider:
    def get_header(self) -> str:
        raise NotImplementedError


class JWTAuthorizationHeaderProvider(AuthorizationHeaderProvider):
    TOKEN_TTL = 30 * 60

    def __init__(self, key, key_id, team_id) -> None:
        self.key = key
        self.key_id = key_id
        self.team_id = team_id

        self.__issued_at = None
        self.__header = None

    def get_header(self):
        now = time.time()
        if not self.__header or self.__issued_at < now - self.TOKEN_TTL:
            self.__issued_at = int(now)
            token = jwt.encode(
                payload={"iss": self.team_id, "iat": self.__issued_at},
                key=self.key,
                algorithm="ES256",
                headers={"kid": self.key_id},
            )
            self.__header = f"bearer {token}"
        return self.__header


class H2Protocol(asyncio.Protocol):
    def __init__(self) -> None:
        self.transport: Optional[asyncio.Transport] = None
        self.conn = H2Connection()
        self.free_channels = ChannelPool(1000)

    def connection_made(self, transport: asyncio.BaseTransport) -> None:
        self.transport = transport  # type: ignore
        self.conn.initiate_connection()
        self.flush()

    def data_received(self, data: bytes) -> None:
        for event in self.conn.receive_data(data):
            if isinstance(event, ResponseReceived):
                headers = dict(event.headers)  # type: ignore
                self.on_response_received(headers)
            elif isinstance(event, DataReceived):
                self.on_data_received(
                    event.data, event.stream_id  # type: ignore
                )
            elif isinstance(event, RemoteSettingsChanged):
                self.on_remote_settings_changed(
                    event.changed_settings  # type: ignore
                )
            elif isinstance(event, StreamEnded):
                self.on_stream_ended(event.stream_id)  # type: ignore
            elif isinstance(event, ConnectionTerminated):
                self.on_connection_terminated(event)
            elif isinstance(event, WindowUpdated):
                pass
            elif isinstance(event, SettingsAcknowledged):
                pass
            else:
                logger.warning("Unknown event: %s", event)
        self.flush()

    def flush(self) -> None:
        assert self.transport is not None
        self.transport.write(self.conn.data_to_send())

    def on_response_received(self, headers: Dict[bytes, bytes]) -> None:
        pass

    def on_data_received(self, data: bytes, stream_id: int) -> None:
        pass

    def on_remote_settings_changed(
        self, changed_settings: Dict[SettingCodes, ChangedSetting]
    ) -> None:
        for setting in changed_settings.values():
            logger.debug("Remote setting changed: %s", setting)
            if setting.setting == SettingCodes.MAX_CONCURRENT_STREAMS:
                self.free_channels.bound = setting.new_value

    def on_stream_ended(self, stream_id: int) -> None:
        if stream_id % 2 == 0:
            logger.warning("End stream: %d", stream_id)
        self.free_channels.release()

    def on_connection_terminated(self, event: ConnectionTerminated) -> None:
        pass


class APNsBaseClientProtocol(H2Protocol):
    APNS_SERVER = "api.push.apple.com"
    INACTIVITY_TIME = 10

    def __init__(
        self,
        apns_topic: str,
        loop: Optional[asyncio.AbstractEventLoop] = None,
        on_connection_lost: Optional[
            Callable[["APNsBaseClientProtocol"], NoReturn]
        ] = None,
        auth_provider: Optional[AuthorizationHeaderProvider] = None,
    ) -> None:
        super(APNsBaseClientProtocol, self).__init__()
        self.apns_topic = apns_topic
        self.loop = loop or asyncio.get_event_loop()
        self.on_connection_lost = on_connection_lost
        self.auth_provider = auth_provider

        self.requests: Dict[str, asyncio.Future[NotificationResult]] = {}
        self.request_streams: Dict[int, str] = {}
        self.request_statuses: Dict[str, str] = {}
        self.inactivity_timer: Optional[asyncio.TimerHandle] = None

    def connection_made(self, transport: asyncio.BaseTransport) -> None:
        super(APNsBaseClientProtocol, self).connection_made(transport)
        self.refresh_inactivity_timer()

    async def send_notification(
        self, request: NotificationRequest
    ) -> NotificationResult:
        stream_id = await self.free_channels.acquire()

        headers = [
            (":method", "POST"),
            (":scheme", "https"),
            (":path", "/3/device/%s" % request.device_token),
            ("host", self.APNS_SERVER),
            ("apns-id", request.notification_id),
        ]
        apns_topic = request.apns_topic or self.apns_topic
        headers.append(("apns-topic", apns_topic))
        if request.time_to_live is not None:
            expiration = int(time.time()) + request.time_to_live
            headers.append(("apns-expiration", str(expiration)))
        if request.priority is not None:
            headers.append(("apns-priority", str(request.priority)))
        if request.collapse_key is not None:
            headers.append(("apns-collapse-id", request.collapse_key))
        if request.push_type is not None:
            headers.append(("apns-push-type", request.push_type.value))
        if self.auth_provider:
            headers.append(("authorization", self.auth_provider.get_header()))

        self.conn.send_headers(stream_id=stream_id, headers=headers)
        try:
            data = json.dumps(request.message, ensure_ascii=False).encode()
            self.conn.send_data(
                stream_id=stream_id,
                data=data,
                end_stream=True,
            )
        except FlowControlError:
            raise

        self.flush()

        future_response: asyncio.Future[NotificationResult] = asyncio.Future()
        self.requests[request.notification_id] = future_response
        self.request_streams[stream_id] = request.notification_id

        response = await future_response
        return response

    def flush(self) -> None:
        assert self.transport is not None
        self.refresh_inactivity_timer()
        self.transport.write(self.conn.data_to_send())

    def refresh_inactivity_timer(self) -> None:
        if self.inactivity_timer:
            self.inactivity_timer.cancel()
        self.inactivity_timer = self.loop.call_later(
            self.INACTIVITY_TIME, self.close
        )

    @property
    def is_busy(self) -> bool:
        return self.free_channels.is_busy

    def close(self) -> None:
        raise NotImplementedError

    def connection_lost(self, exc: Optional[Exception]) -> None:
        logger.debug("Connection %s lost! Error: %s", self, exc)

        if self.inactivity_timer:
            self.inactivity_timer.cancel()

        if self.on_connection_lost:
            self.on_connection_lost(self)

        closed_connection = ConnectionClosed(str(exc))
        for request in self.requests.values():
            request.set_exception(closed_connection)
        self.free_channels.destroy(closed_connection)

    def on_response_received(self, headers: Dict[bytes, bytes]) -> None:
        notification_id = headers.get(b"apns-id", b"").decode("utf8")
        status = headers.get(b":status", b"").decode("utf8")
        if status == APNS_RESPONSE_CODE.SUCCESS:
            request = self.requests.pop(notification_id, None)
            if request:
                result = NotificationResult(notification_id, status)
                request.set_result(result)
            else:
                logger.warning(
                    "Got response for unknown notification request %s",
                    notification_id,
                )
        else:
            self.request_statuses[notification_id] = status

    def on_data_received(self, raw_data: bytes, stream_id: int) -> None:
        data = json.loads(raw_data.decode())
        reason = data.get("reason", "")
        timestamp = data.get("timestamp")

        if not reason:
            return

        notification_id = self.request_streams.pop(stream_id, None)
        if notification_id:
            request = self.requests.pop(notification_id, None)
            if request:
                # TODO: Теоретически здесь может быть ошибка, если нет ключа
                status = self.request_statuses.pop(notification_id)
                result = NotificationResult(
                    notification_id,
                    status,
                    description=reason,
                    timestamp=timestamp,
                )
                request.set_result(result)
            else:
                logger.warning("Could not find request %s", notification_id)
        else:
            logger.warning(
                "Could not find notification by stream %s", stream_id
            )

    def on_connection_terminated(self, event: ConnectionTerminated):
        logger.warning(
            "Connection %s terminated: code=%s, additional_data=%s, "
            "last_stream_id=%s",
            self,
            event.error_code,
            event.additional_data,
            event.last_stream_id,
        )
        self.close()


class APNsTLSClientProtocol(APNsBaseClientProtocol):
    APNS_PORT = 443

    def close(self) -> None:
        if self.inactivity_timer:
            self.inactivity_timer.cancel()
        logger.debug("Closing connection %s", self)
        if self.transport is not None:
            self.transport.close()


class APNsProductionClientProtocol(APNsTLSClientProtocol):
    APNS_SERVER = "api.push.apple.com"


class APNsDevelopmentClientProtocol(APNsTLSClientProtocol):
    APNS_SERVER = "api.development.push.apple.com"


class APNsBaseConnectionPool:
    def __init__(
        self,
        topic: Optional[str] = None,
        max_connections: int = 10,
        max_connection_attempts: int = 5,
        use_sandbox: bool = False,
        proxy_host: Optional[str] = None,
        proxy_port: Optional[int] = None,
    ) -> None:
        self.apns_topic = topic
        self.max_connections = max_connections
        self.protocol_class: Type[APNsTLSClientProtocol]
        if use_sandbox:
            self.protocol_class = APNsDevelopmentClientProtocol
        else:
            self.protocol_class = APNsProductionClientProtocol

        self.loop = asyncio.get_event_loop()
        self.connections: List[APNsBaseClientProtocol] = []
        self._lock = asyncio.Lock()
        self.max_connection_attempts = max_connection_attempts
        self.ssl_context: Optional[ssl.SSLContext] = None

        self.proxy_host = proxy_host
        self.proxy_port = proxy_port

    async def create_connection(self) -> APNsBaseClientProtocol:
        raise NotImplementedError

    def close(self) -> None:
        for connection in self.connections:
            connection.close()

    def discard_connection(self, connection: APNsBaseClientProtocol) -> None:
        logger.debug("Connection %s discarded", connection)
        self.connections.remove(connection)
        logger.info("Connection released (total: %d)", len(self.connections))

    async def acquire(self) -> APNsBaseClientProtocol:
        for connection in self.connections:
            if not connection.is_busy:
                return connection

        async with self._lock:
            for connection in self.connections:
                if not connection.is_busy:
                    return connection
            if len(self.connections) < self.max_connections:
                try:
                    connection = await self.create_connection()
                except Exception as e:
                    logger.error("Could not connect to server: %s", str(e))
                    raise ConnectionError()
                self.connections.append(connection)
                logger.info(
                    "Connection established (total: %d)", len(self.connections)
                )
                return connection

        logger.warning(
            "Pool is completely busy and has hit max connections, retrying..."
        )
        while True:
            await asyncio.sleep(0.01)
            for connection in self.connections:
                if not connection.is_busy:
                    return connection

    async def send_notification(
        self, request: NotificationRequest
    ) -> NotificationResult:
        attempts = 0
        while attempts < self.max_connection_attempts:
            attempts += 1
            logger.debug(
                "Notification %s: waiting for connection",
                request.notification_id,
            )
            try:
                connection = await self.acquire()
            except ConnectionError:
                logger.warning(
                    "Could not send notification %s: " "ConnectionError",
                    request.notification_id,
                )
                await asyncio.sleep(1)
                continue
            logger.debug(
                "Notification %s: connection %s acquired",
                request.notification_id,
                connection,
            )
            try:
                response = await connection.send_notification(request)
                return response
            except NoAvailableStreamIDError:
                connection.close()
            except ConnectionClosed:
                logger.warning(
                    "Could not send notification %s: " "ConnectionClosed",
                    request.notification_id,
                )
            except FlowControlError:
                logger.debug(
                    "Got FlowControlError for notification %s",
                    request.notification_id,
                )
                await asyncio.sleep(1)
        logger.error("Failed to send after %d attempts.", attempts)
        raise MaxAttemptsExceeded

    async def _create_proxy_connection(
        self, apns_protocol_factory
    ) -> APNsBaseClientProtocol:
        assert self.proxy_host is not None, "proxy_host must be set"
        assert self.proxy_port is not None, "proxy_port must be set"
        assert self.ssl_context is not None, "ssl_context must be set"

        _, protocol = await self.loop.create_connection(
            protocol_factory=partial(
                HttpProxyProtocol,
                self.protocol_class.APNS_SERVER,
                self.protocol_class.APNS_PORT,
                self.loop,
                self.ssl_context,
                apns_protocol_factory,
            ),
            host=self.proxy_host,
            port=self.proxy_port,
        )
        await protocol.apns_connection_ready.wait()

        assert (
            protocol.apns_protocol is not None
        ), "protocol.apns_protocol could not be set"
        return protocol.apns_protocol


class APNsCertConnectionPool(APNsBaseConnectionPool):
    def __init__(
        self,
        cert_file: str,
        topic: Optional[str] = None,
        max_connections: int = 10,
        max_connection_attempts: int = 5,
        use_sandbox: bool = False,
        no_cert_validation: bool = False,
        ssl_context: Optional[ssl.SSLContext] = None,
        proxy_host: Optional[str] = None,
        proxy_port: Optional[int] = None,
    ) -> None:
        super(APNsCertConnectionPool, self).__init__(
            topic=topic,
            max_connections=max_connections,
            max_connection_attempts=max_connection_attempts,
            use_sandbox=use_sandbox,
            proxy_host=proxy_host,
            proxy_port=proxy_port,
        )

        self.cert_file = cert_file
        self.ssl_context = ssl_context or ssl.create_default_context()
        if no_cert_validation:
            self.ssl_context.check_hostname = False
            self.ssl_context.verify_mode = ssl.CERT_NONE
        self.ssl_context.load_cert_chain(cert_file)

        if not self.apns_topic:
            with open(self.cert_file, "rb") as f:
                body = f.read()
                cert = OpenSSL.crypto.load_certificate(  # type: ignore
                    OpenSSL.crypto.FILETYPE_PEM, body  # type: ignore
                )
                self.apns_topic = cert.get_subject().UID

    async def create_connection(self) -> APNsBaseClientProtocol:
        apns_protocol_factory = partial(
            self.protocol_class,
            self.apns_topic,  # type: ignore
            self.loop,
            self.discard_connection,  # type: ignore
        )

        if self.proxy_host and self.proxy_port:
            return await self._create_proxy_connection(apns_protocol_factory)
        else:
            return await self._create_connection(apns_protocol_factory)

    async def _create_connection(
        self, apns_protocol_factory
    ) -> APNsBaseClientProtocol:
        _, protocol = await self.loop.create_connection(
            protocol_factory=apns_protocol_factory,
            host=self.protocol_class.APNS_SERVER,
            port=self.protocol_class.APNS_PORT,
            ssl=self.ssl_context,
        )
        return protocol


class APNsKeyConnectionPool(APNsBaseConnectionPool):
    def __init__(
        self,
        key: str,
        key_id: str,
        team_id: str,
        topic: str,
        max_connections: int = 10,
        max_connection_attempts: int = 5,
        use_sandbox: bool = False,
        ssl_context: Optional[ssl.SSLContext] = None,
        proxy_host: Optional[str] = None,
        proxy_port: Optional[int] = None,
    ) -> None:
        super(APNsKeyConnectionPool, self).__init__(
            topic=topic,
            max_connections=max_connections,
            max_connection_attempts=max_connection_attempts,
            use_sandbox=use_sandbox,
            proxy_host=proxy_host,
            proxy_port=proxy_port,
        )

        self.ssl_context = ssl_context or ssl.create_default_context()

        self.key = key
        self.key_id = key_id
        self.team_id = team_id

    async def create_connection(self) -> APNsBaseClientProtocol:
        auth_provider = JWTAuthorizationHeaderProvider(
            key=self.key, key_id=self.key_id, team_id=self.team_id
        )
        apns_protocol_factory = partial(
            self.protocol_class,
            self.apns_topic,  # type: ignore
            self.loop,
            self.discard_connection,  # type: ignore
            auth_provider,
        )

        if self.proxy_host and self.proxy_port:
            return await self._create_proxy_connection(apns_protocol_factory)
        else:
            return await self._create_connection(apns_protocol_factory)

    async def _create_connection(
        self, apns_protocol_factory
    ) -> APNsBaseClientProtocol:
        _, protocol = await self.loop.create_connection(
            protocol_factory=apns_protocol_factory,
            host=self.protocol_class.APNS_SERVER,
            port=self.protocol_class.APNS_PORT,
            ssl=self.ssl_context,
        )
        return protocol


class HttpProxyProtocol(asyncio.Protocol):
    def __init__(
        self,
        apns_host: str,
        apns_port: int,
        loop: asyncio.AbstractEventLoop,
        ssl_context: ssl.SSLContext,
        protocol_factory,
    ):
        self.apns_host = apns_host
        self.apns_port = apns_port
        self.buffer = bytearray()
        self.loop = loop
        self.ssl_context = ssl_context
        self.apns_protocol_factory = protocol_factory
        self.apns_protocol: Optional[APNsBaseClientProtocol] = None
        self.transport = None
        self.apns_connection_ready = (
            asyncio.Event()
        )  # Event to signal APNs readiness

    def connection_made(self, transport):
        logger.debug(
            "Proxy connection made.",
        )
        self.transport = transport
        connect_request = (
            f"CONNECT {self.apns_host}:{self.apns_port} "
            f"HTTP/1.1\r\nHost: "
            f"{self.apns_host}\r\nConnection: close\r\n\r\n"
        )
        self.transport.write(connect_request.encode("utf-8"))

    def data_received(self, data):
        # Data is usually received in bytes,
        # so you might want to decode or process it
        logger.debug("Raw data received: %s", data)
        self.buffer.extend(data)
        # some proxies send "HTTP/1.1 200 Connection established"
        # others "HTTP/1.1 200 Connected"
        if b"HTTP/1.1 200 Connect" in data:
            logger.debug(
                "Proxy tunnel established.",
            )
            asyncio.create_task(self.create_apns_connection())
        else:
            logger.debug(
                "Data received (before APNs connection establishment): %s",
                data.decode(),
            )

    async def create_apns_connection(self):
        # Use the existing transport to create a new APNs connection
        logger.debug(
            "Initiating APNs connection.",
        )
        sock = self.transport.get_extra_info("socket")
        _, self.apns_protocol = await self.loop.create_connection(
            self.apns_protocol_factory,
            server_hostname=self.apns_host,
            ssl=self.ssl_context,
            sock=sock,
        )
        # Signal that APNs connection is ready
        self.apns_connection_ready.set()

    def connection_lost(self, exc):
        logger.debug(
            "Proxy connection lost.",
        )
        self.transport.close()


================================================
FILE: aioapns/exceptions.py
================================================
class ConnectionClosed(Exception):
    pass


class ConnectionError(Exception):
    pass


class MaxAttemptsExceeded(Exception):
    pass


================================================
FILE: aioapns/logging.py
================================================
import logging

logging.getLogger("hpack").setLevel(logging.CRITICAL)

logger = logging.getLogger("aioapns")


def set_hpack_debugging(value: bool) -> None:
    if value:
        logging.getLogger("hpack").setLevel(logging.DEBUG)


================================================
FILE: aioapns/py.typed
================================================


================================================
FILE: examples/client.py
================================================
import asyncio
import logging

import uvloop

from aioapns import APNs, NotificationRequest


def setup_logger(log_level):
    log_level = getattr(logging, log_level)
    logging.basicConfig(
        format="[%(asctime)s] %(levelname)8s %(module)6s:%(lineno)03d %(message)s",
        level=log_level,
    )


if __name__ == "__main__":
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    setup_logger("INFO")

    client_cert = "apns-production-cert.pem"
    device_token = "<DEVICE_TOKEN>"
    message = {
        "aps": {
            "alert": "Hello from APNs Tester.",
            "badge": "1",
            "sound": "default",
        }
    }

    apns = APNs(client_cert)

    async def send_request():
        request = NotificationRequest(
            device_token=device_token, message=message
        )
        await apns.send_notification(request)

    async def main():
        send_requests = [send_request() for _ in range(1000)]
        import time

        t = time.time()
        await asyncio.wait(send_requests)
        print("Done: %s" % (time.time() - t))
        print()

    try:
        loop = asyncio.get_event_loop()
        asyncio.ensure_future(main())
        loop.run_forever()
    except KeyboardInterrupt:
        pass


================================================
FILE: pyproject.toml
================================================
[mypy]
ignore_missing_imports = true

[flake8]
max-line-length = 79

[tool.isort]
multi_line_output = 3
include_trailing_comma = true
ensure_newline_before_comments = true

[tool.black]
line-length = 79
target-version = ['py38', 'py39', 'py310', 'py311']


================================================
FILE: requirements-dev.txt
================================================
black>=24.3.0
isort>=5.13.2
flake8>=7.0.0
mypy>=1.9.0

# mypy
types-pyOpenSSL

================================================
FILE: setup.cfg
================================================
[mypy]
ignore_missing_imports = True


================================================
FILE: setup.py
================================================
#!/usr/bin/env python

from setuptools import find_packages, setup

setup(
    name="aioapns",
    version="4.0",
    description="An efficient APNs Client Library for Python/asyncio",
    long_description=open("README.rst").read(),
    platforms="all",
    classifiers=[
        "License :: OSI Approved :: Apache Software License",
        "Intended Audience :: Developers",
        "Operating System :: MacOS :: MacOS X",
        "Operating System :: POSIX",
        "Programming Language :: Python :: 3 :: Only",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3.11",
        "Development Status :: 5 - Production/Stable",
    ],
    license="Apache License, Version 2.0",
    author="Alexander Tikhonov",
    author_email="random.gauss@gmail.com",
    url="https://github.com/Fatal1ty/aioapns",
    packages=find_packages(exclude=("tests",)),
    package_data={"aioapns": ["py.typed"]},
    install_requires=[
        "h2>=4.0.0",
        "pyOpenSSL>=17.5.0",
        "pyjwt>=2.0.0",
    ],
)
Download .txt
gitextract_rn18lmov/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── LICENSE
├── README.rst
├── aioapns/
│   ├── __init__.py
│   ├── client.py
│   ├── common.py
│   ├── connection.py
│   ├── exceptions.py
│   ├── logging.py
│   └── py.typed
├── examples/
│   └── client.py
├── pyproject.toml
├── requirements-dev.txt
├── setup.cfg
└── setup.py
Download .txt
SYMBOL INDEX (79 symbols across 6 files)

FILE: aioapns/client.py
  class APNs (line 13) | class APNs:
    method __init__ (line 14) | def __init__(
    method send_notification (line 69) | async def send_notification(

FILE: aioapns/common.py
  class PushType (line 10) | class PushType(Enum):
  class NotificationRequest (line 20) | class NotificationRequest:
    method __init__ (line 32) | def __init__(
  class NotificationResult (line 54) | class NotificationResult:
    method __init__ (line 57) | def __init__(
    method is_successful (line 70) | def is_successful(self) -> bool:
  class DynamicBoundedSemaphore (line 74) | class DynamicBoundedSemaphore(asyncio.BoundedSemaphore):
    method bound (line 78) | def bound(self) -> int:
    method bound (line 82) | def bound(self, new_bound: int) -> None:
    method release (line 93) | def release(self) -> None:
    method destroy (line 99) | def destroy(self, exc: Exception) -> None:
  class APNS_RESPONSE_CODE (line 106) | class APNS_RESPONSE_CODE:

FILE: aioapns/connection.py
  class ChannelPool (line 37) | class ChannelPool(DynamicBoundedSemaphore):
    method __init__ (line 38) | def __init__(self, *args: Any, **kwargs: Any) -> None:
    method acquire (line 42) | async def acquire(self) -> int:  # type: ignore
    method is_busy (line 50) | def is_busy(self) -> bool:
  class AuthorizationHeaderProvider (line 54) | class AuthorizationHeaderProvider:
    method get_header (line 55) | def get_header(self) -> str:
  class JWTAuthorizationHeaderProvider (line 59) | class JWTAuthorizationHeaderProvider(AuthorizationHeaderProvider):
    method __init__ (line 62) | def __init__(self, key, key_id, team_id) -> None:
    method get_header (line 70) | def get_header(self):
  class H2Protocol (line 84) | class H2Protocol(asyncio.Protocol):
    method __init__ (line 85) | def __init__(self) -> None:
    method connection_made (line 90) | def connection_made(self, transport: asyncio.BaseTransport) -> None:
    method data_received (line 95) | def data_received(self, data: bytes) -> None:
    method flush (line 120) | def flush(self) -> None:
    method on_response_received (line 124) | def on_response_received(self, headers: Dict[bytes, bytes]) -> None:
    method on_data_received (line 127) | def on_data_received(self, data: bytes, stream_id: int) -> None:
    method on_remote_settings_changed (line 130) | def on_remote_settings_changed(
    method on_stream_ended (line 138) | def on_stream_ended(self, stream_id: int) -> None:
    method on_connection_terminated (line 143) | def on_connection_terminated(self, event: ConnectionTerminated) -> None:
  class APNsBaseClientProtocol (line 147) | class APNsBaseClientProtocol(H2Protocol):
    method __init__ (line 151) | def __init__(
    method connection_made (line 171) | def connection_made(self, transport: asyncio.BaseTransport) -> None:
    method send_notification (line 175) | async def send_notification(
    method flush (line 221) | def flush(self) -> None:
    method refresh_inactivity_timer (line 226) | def refresh_inactivity_timer(self) -> None:
    method is_busy (line 234) | def is_busy(self) -> bool:
    method close (line 237) | def close(self) -> None:
    method connection_lost (line 240) | def connection_lost(self, exc: Optional[Exception]) -> None:
    method on_response_received (line 254) | def on_response_received(self, headers: Dict[bytes, bytes]) -> None:
    method on_data_received (line 270) | def on_data_received(self, raw_data: bytes, stream_id: int) -> None:
    method on_connection_terminated (line 298) | def on_connection_terminated(self, event: ConnectionTerminated):
  class APNsTLSClientProtocol (line 310) | class APNsTLSClientProtocol(APNsBaseClientProtocol):
    method close (line 313) | def close(self) -> None:
  class APNsProductionClientProtocol (line 321) | class APNsProductionClientProtocol(APNsTLSClientProtocol):
  class APNsDevelopmentClientProtocol (line 325) | class APNsDevelopmentClientProtocol(APNsTLSClientProtocol):
  class APNsBaseConnectionPool (line 329) | class APNsBaseConnectionPool:
    method __init__ (line 330) | def __init__(
    method create_connection (line 356) | async def create_connection(self) -> APNsBaseClientProtocol:
    method close (line 359) | def close(self) -> None:
    method discard_connection (line 363) | def discard_connection(self, connection: APNsBaseClientProtocol) -> None:
    method acquire (line 368) | async def acquire(self) -> APNsBaseClientProtocol:
    method send_notification (line 398) | async def send_notification(
    method _create_proxy_connection (line 441) | async def _create_proxy_connection(
  class APNsCertConnectionPool (line 468) | class APNsCertConnectionPool(APNsBaseConnectionPool):
    method __init__ (line 469) | def __init__(
    method create_connection (line 505) | async def create_connection(self) -> APNsBaseClientProtocol:
    method _create_connection (line 518) | async def _create_connection(
  class APNsKeyConnectionPool (line 530) | class APNsKeyConnectionPool(APNsBaseConnectionPool):
    method __init__ (line 531) | def __init__(
    method create_connection (line 559) | async def create_connection(self) -> APNsBaseClientProtocol:
    method _create_connection (line 576) | async def _create_connection(
  class HttpProxyProtocol (line 588) | class HttpProxyProtocol(asyncio.Protocol):
    method __init__ (line 589) | def __init__(
    method connection_made (line 609) | def connection_made(self, transport):
    method data_received (line 621) | def data_received(self, data):
    method create_apns_connection (line 639) | async def create_apns_connection(self):
    method connection_lost (line 654) | def connection_lost(self, exc):

FILE: aioapns/exceptions.py
  class ConnectionClosed (line 1) | class ConnectionClosed(Exception):
  class ConnectionError (line 5) | class ConnectionError(Exception):
  class MaxAttemptsExceeded (line 9) | class MaxAttemptsExceeded(Exception):

FILE: aioapns/logging.py
  function set_hpack_debugging (line 8) | def set_hpack_debugging(value: bool) -> None:

FILE: examples/client.py
  function setup_logger (line 9) | def setup_logger(log_level):
  function send_request (line 33) | async def send_request():
  function main (line 39) | async def main():
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (50K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 43,
    "preview": "custom: ['https://coindrop.to/tikhonov_a']\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 637,
    "preview": "name: tests\n\non:\n  push:\n    branches:\n      - '*'\n  pull_request:\n    branches:\n      - master\n\njobs:\n  tests:\n    runs"
  },
  {
    "path": ".gitignore",
    "chars": 718,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\n"
  },
  {
    "path": "LICENSE",
    "chars": 11348,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.rst",
    "chars": 2758,
    "preview": "aioapns - An efficient APNs Client Library for Python/asyncio\n=========================================================="
  },
  {
    "path": "aioapns/__init__.py",
    "chars": 326,
    "preview": "from aioapns.client import APNs\nfrom aioapns.common import (\n    PRIORITY_HIGH,\n    PRIORITY_NORMAL,\n    NotificationReq"
  },
  {
    "path": "aioapns/client.py",
    "chars": 2927,
    "preview": "from ssl import SSLContext\nfrom typing import Awaitable, Callable, Optional\n\nfrom aioapns.common import NotificationRequ"
  },
  {
    "path": "aioapns/common.py",
    "chars": 3093,
    "preview": "import asyncio\nfrom enum import Enum\nfrom typing import Any, Dict, Optional\nfrom uuid import uuid4\n\nPRIORITY_NORMAL = \"5"
  },
  {
    "path": "aioapns/connection.py",
    "chars": 22909,
    "preview": "import asyncio\nimport json\nimport ssl\nimport time\nfrom functools import partial\nfrom typing import Any, Callable, Dict, "
  },
  {
    "path": "aioapns/exceptions.py",
    "chars": 138,
    "preview": "class ConnectionClosed(Exception):\n    pass\n\n\nclass ConnectionError(Exception):\n    pass\n\n\nclass MaxAttemptsExceeded(Exc"
  },
  {
    "path": "aioapns/logging.py",
    "chars": 230,
    "preview": "import logging\n\nlogging.getLogger(\"hpack\").setLevel(logging.CRITICAL)\n\nlogger = logging.getLogger(\"aioapns\")\n\n\ndef set_h"
  },
  {
    "path": "aioapns/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "examples/client.py",
    "chars": 1261,
    "preview": "import asyncio\nimport logging\n\nimport uvloop\n\nfrom aioapns import APNs, NotificationRequest\n\n\ndef setup_logger(log_level"
  },
  {
    "path": "pyproject.toml",
    "chars": 255,
    "preview": "[mypy]\nignore_missing_imports = true\n\n[flake8]\nmax-line-length = 79\n\n[tool.isort]\nmulti_line_output = 3\ninclude_trailing"
  },
  {
    "path": "requirements-dev.txt",
    "chars": 77,
    "preview": "black>=24.3.0\nisort>=5.13.2\nflake8>=7.0.0\nmypy>=1.9.0\n\n# mypy\ntypes-pyOpenSSL"
  },
  {
    "path": "setup.cfg",
    "chars": 37,
    "preview": "[mypy]\nignore_missing_imports = True\n"
  },
  {
    "path": "setup.py",
    "chars": 1141,
    "preview": "#!/usr/bin/env python\n\nfrom setuptools import find_packages, setup\n\nsetup(\n    name=\"aioapns\",\n    version=\"4.0\",\n    de"
  }
]

About this extraction

This page contains the full source code of the Fatal1ty/aioapns GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (46.8 KB), approximately 10.5k tokens, and a symbol index with 79 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!