Skip to main content

app-store-server-library-python-async

Project description

Apple App Store Server Python Library Async Version

The Python server library for the App Store Server API and App Store Server Notifications. This library has been rewritten by Bahadır Araz to use asynchronous code. This update prevents blocking when handling concurrent API requests and includes instructions on how to use the library with Pydantic for improved data validation with modern Python practices.

Table of Contents

  1. Installation
  2. Documentation
  3. Usage
  4. Support

Installation

Requirements

  • Python 3.7+

pip

pip install app-store-server-library-async

Documentation

Documentation

WWDC Video

Obtaining an In-App Purchase key from App Store Connect

To use the App Store Server API or create promotional offer signatures, a signing key downloaded from App Store Connect is required. To obtain this key, you must have the Admin role. Go to Users and Access > Integrations > In-App Purchase. Here you can create and manage keys, as well as find your issuer ID. When using a key, you'll need the key ID and issuer ID as well.

Obtaining Apple Root Certificates

Download and store the root certificates found in the Apple Root Certificates section of the Apple PKI site. Provide these certificates as an array to a SignedDataVerifier to allow verifying the signed data comes from Apple.

Usage

API Usage

from appstoreserverlibraryasync.api_client import AsyncAppStoreServerAPIClient, APIException
from appstoreserverlibraryasync.models.Environment import Environment


def read_private_key(file_path):
    with open(file_path, "r") as file:
        return file.read()


private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8")  # Implementation will vary

key_id = "ABCDEFGHIJ"
issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55"
bundle_id = "com.example"
environment = Environment.SANDBOX

client = AsyncAppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)


async def main():
    try:
        response = await client.request_test_notification()
        print(response)
    except APIException as e:
        print(e)

Verification Usage

from appstoreserverlibraryasync.models.Environment import Environment
from appstoreserverlibraryasync.signed_data_verifier import VerificationException, SignedDataVerifier


def load_root_certificates():
    cert_paths = [
        f"AppleComputerRootCertificate.cer",
        f"AppleRootCA-G3.cer",
        f"AppleRootCA-G2.cer",
        f"AppleIncRootCertificate.cer",
    ]
    certs = []
    for path in cert_paths:
        file = open(path, "rb")
        cert = file.read()
        file.close()
        certs.append(cert)
    return certs


async def main():
    root_certificates = load_root_certificates()
    enable_online_checks = True
    bundle_id = "com.example"
    environment = Environment.SANDBOX
    app_apple_id = None  # appAppleId must be provided for the Production environment
    signed_data_verifier = SignedDataVerifier(root_certificates, enable_online_checks, environment, bundle_id,
                                              app_apple_id)

    try:
        signed_notification = "ey.."
        payload = await signed_data_verifier.verify_and_decode_notification(signed_notification)
        print(payload)
    except VerificationException as e:
        print(e)

Receipt Usage

from appstoreserverlibraryasync.api_client import AsyncAppStoreServerAPIClient, APIException
from appstoreserverlibraryasync.models.Environment import Environment
from appstoreserverlibraryasync.receipt_utility import ReceiptUtility
from appstoreserverlibraryasync.models.HistoryResponse import HistoryResponse
from appstoreserverlibraryasync.models.TransactionHistoryRequest import TransactionHistoryRequest, ProductType, Order


def read_private_key(file_path):
    with open(file_path, "r") as file:
        return file.read()


private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8")  # Implementation will vary

key_id = "ABCDEFGHIJ"
issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55"
bundle_id = "com.example"
environment = Environment.SANDBOX

client = AsyncAppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)
receipt_util = ReceiptUtility()
app_receipt = "MI.."


async def extract_transaction_receipts_from_id(app_receipt):
    try:
        transaction_id = receipt_util.extract_transaction_id_from_app_receipt(app_receipt)
        if transaction_id != None:
            transactions = []
            response: HistoryResponse = None
            request: TransactionHistoryRequest = TransactionHistoryRequest(
                sort=Order.ASCENDING,
                revoked=False,
                productTypes=[ProductType.AUTO_RENEWABLE]
            )
            while response is None or response.hasMore:
                revision = response.revision if response is not None else None
                response = await client.get_transaction_history(transaction_id, revision, request)
                for transaction in response.signedTransactions:
                    transactions.append(transaction)
            print(transactions)
    except APIException as e:
        print(e)

Promotional Offer Signature Creation

from appstoreserverlibraryasync.promotional_offer import PromotionalOfferSignatureCreator
import time


def read_private_key(file_path):
    with open(file_path, "r") as file:
        return file.read()


private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8")  # Implementation will vary

key_id = "ABCDEFGHIJ"
bundle_id = "com.example"

promotion_code_signature_generator = PromotionalOfferSignatureCreator(private_key, key_id, bundle_id)

product_id = "<product_id>"
subscription_offer_id = "<subscription_offer_id>"
application_username = "<application_username>"
nonce = "<nonce>"
timestamp = round(time.time() * 1000)
base64_encoded_signature = promotion_code_signature_generator.create_signature(product_id, subscription_offer_id,
                                                                               application_username, nonce, timestamp)

Pydantic Example

Apple has interestingly chosen to use attrs library. Here is an example of how you can convert it to Pydantic.

from appstoreserverlibraryasync.models.JWSTransactionDecodedPayload import JWSTransactionDecodedPayload, Type,
    InAppOwnershipType, RevocationReason, OfferType, Environment, TransactionReason, OfferDiscountType
from pydantic import BaseModel
import attr
from typing import Optional

example_payload = JWSTransactionDecodedPayload(
    originalTransactionId="1000000077803123",
    transactionId="1000000077803124",
    webOrderLineItemId="1000000077803125",
    bundleId="com.example.app",
    productId="com.example.product",
    subscriptionGroupIdentifier="12345678",
    purchaseDate=1622548800000,
    originalPurchaseDate=1622548800000,
    expiresDate=1625130800000,
    quantity=1,
    type=Type("Auto-Renewable Subscription"),
    rawType="1",
    appAccountToken="E7DE6CBE-E0C2-4B52-A00C-E0C2E7DE6CBE",
    inAppOwnershipType=InAppOwnershipType("PURCHASED"),
    rawInAppOwnershipType="PURCHASED",
    signedDate=1622548800000,
    revocationReason=RevocationReason(1),
    rawRevocationReason=0,
    revocationDate=1622548800000,
    isUpgraded=False,
    offerType=OfferType(1),
    rawOfferType=1,
    offerIdentifier="offer123",
    environment=Environment("Sandbox"),
    rawEnvironment="PRODUCTION",
    storefront="USA",
    storefrontId="143441",
    transactionReason=TransactionReason("PURCHASE"),
    rawTransactionReason="PURCHASE",
    currency="USD",
    price=499,
    offerDiscountType=OfferDiscountType("FREE_TRIAL"),
    rawOfferDiscountType="FREE_TRIAL"
)


class PydanticJWSTransactionDecodedPayload(BaseModel):
    originalTransactionId: Optional[str] = None
    transactionId: Optional[str] = None
    webOrderLineItemId: Optional[str] = None
    bundleId: Optional[str] = None
    productId: Optional[str] = None
    subscriptionGroupIdentifier: Optional[str] = None
    purchaseDate: Optional[int] = None
    originalPurchaseDate: Optional[int] = None
    expiresDate: Optional[int] = None
    quantity: Optional[int] = None
    type: Optional[str] = None
    rawType: Optional[str] = None
    appAccountToken: Optional[str] = None
    inAppOwnershipType: Optional[str] = None
    rawInAppOwnershipType: Optional[str] = None
    signedDate: Optional[int] = None
    revocationReason: Optional[int] = None
    rawRevocationReason: Optional[int] = None
    revocationDate: Optional[int] = None
    isUpgraded: Optional[bool] = None
    offerType: Optional[int] = None
    rawOfferType: Optional[int] = None
    offerIdentifier: Optional[str] = None
    environment: Optional[str] = None
    rawEnvironment: Optional[str] = None
    storefront: Optional[str] = None
    storefrontId: Optional[str] = None
    transactionReason: Optional[str] = None
    rawTransactionReason: Optional[str] = None
    currency: Optional[str] = None
    price: Optional[int] = None
    offerDiscountType: Optional[str] = None
    rawOfferDiscountType: Optional[str] = None


# Convert to Pydantic
pydanctic_payload = PydanticJWSTransactionDecodedPayload(**attr.asdict(example_payload))
print(pydanctic_payload)

Support

Only the latest major version of the library will receive updates, including security updates. Therefore, it is recommended to update to new major versions.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

Built Distribution

File details

Details for the file app_store_server_library_python_async-0.1.1.tar.gz.

File metadata

File hashes

Hashes for app_store_server_library_python_async-0.1.1.tar.gz
Algorithm Hash digest
SHA256 8df4ff761934570c5eed5ddec160ed2af00c15b338aa4aaa674542b7c47c39c7
MD5 8fc42416c3c0bad58a7aab5b6c7a861b
BLAKE2b-256 f894fb14f3f80708821ab11ece42bc95e7187a5b9e3b641ad7ba62d7c1085350

See more details on using hashes here.

File details

Details for the file app_store_server_library_python_async-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for app_store_server_library_python_async-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 1cdf69cf962997aaf212c76a5b313b5f062c44d32c6deb7d2f3a3c40fb95b0e3
MD5 d99504823d17927f15761653368c35e4
BLAKE2b-256 5377b9332152aa7146fa6d34953696fe43248138218f682f2d4037f87773d79d

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page