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
Installation
Requirements
- Python 3.7+
pip
pip install app-store-server-library-async
Documentation
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
Release history Release notifications | RSS feed
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
- Download URL: app_store_server_library_python_async-0.1.1.tar.gz
- Upload date:
- Size: 63.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.0.0 CPython/3.12.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8df4ff761934570c5eed5ddec160ed2af00c15b338aa4aaa674542b7c47c39c7 |
|
MD5 | 8fc42416c3c0bad58a7aab5b6c7a861b |
|
BLAKE2b-256 | f894fb14f3f80708821ab11ece42bc95e7187a5b9e3b641ad7ba62d7c1085350 |
File details
Details for the file app_store_server_library_python_async-0.1.1-py3-none-any.whl
.
File metadata
- Download URL: app_store_server_library_python_async-0.1.1-py3-none-any.whl
- Upload date:
- Size: 63.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.0.0 CPython/3.12.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1cdf69cf962997aaf212c76a5b313b5f062c44d32c6deb7d2f3a3c40fb95b0e3 |
|
MD5 | d99504823d17927f15761653368c35e4 |
|
BLAKE2b-256 | 5377b9332152aa7146fa6d34953696fe43248138218f682f2d4037f87773d79d |