Skip to content

Commit

Permalink
[Experimental] asyncio support
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowone committed Jul 26, 2017
1 parent 518ea31 commit d47552e
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 110 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -15,7 +15,7 @@ install:
- "pip install flake8 python-coveralls sphinx '.[tests]'"
# command to run tests
script:
- "flake8 --ignore=E501 ."
- "flake8 --ignore=E501,E999 ."
- "pytest --cov=itunesiap -vv tests/"
- "python -msphinx -M html docs build"
after_success:
Expand Down
1 change: 1 addition & 0 deletions docs/request.rst
Expand Up @@ -5,6 +5,7 @@ Request

.. autoclass:: itunesiap.request.Request
:members:
:inherited-members:

.. automodule:: itunesiap.exceptions

Expand Down
4 changes: 2 additions & 2 deletions itunesiap/__init__.py
Expand Up @@ -12,7 +12,7 @@

from .request import Request
from .receipt import Response, Receipt, InApp
from .shortcut import verify
from .shortcut import verify, aioverify

from . import exceptions
from . import environment
Expand All @@ -33,5 +33,5 @@


__all__ = (
'Request', 'Response', 'Receipt', 'InApp', 'verify',
'Request', 'Response', 'Receipt', 'InApp', 'verify', 'aioverify',
'exceptions', 'exc', 'environment', 'env')
123 changes: 20 additions & 103 deletions itunesiap/request.py
@@ -1,30 +1,19 @@
""":mod:`itunesiap.request`"""
import json
import functools

import requests
from itunesiap.verify_requests import RequestsVerify

from . import receipt
from . import exceptions
from .environment import Environment
try:
from itunesiap.verify_aiohttp import AiohttpVerify
except (SyntaxError, ImportError):
class AiohttpVerify(object):
pass

RECEIPT_PRODUCTION_VALIDATION_URL = "https://buy.itunes.apple.com/verifyReceipt"
RECEIPT_SANDBOX_VALIDATION_URL = "https://sandbox.itunes.apple.com/verifyReceipt"
STATUS_SANDBOX_RECEIPT_ERROR = 21007

class RequestBase(object):

class Request(object):
"""Validation request with raw receipt.
Use `verify` method to try verification and get Receipt or exception.
For detail, see also the Apple document: `<https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html>`_.
:param str receipt_data: An iTunes receipt data as Base64 encoded string.
:param str password: Only used for receipts that contain auto-renewable subscriptions. Your app's shared secret (a hexadecimal string).
:param bool exclude_old_transactions: Only used for iOS7 style app receipts that contain auto-renewable or non-renewing subscriptions. If value is true, response includes only the latest renewal transaction for any subscriptions.
:param proxy_url: A proxy url to access the iTunes validation url.
(It is an attribute of :func:`verify` but misplaced here)
"""
PRODUCTION_VALIDATION_URL = "https://buy.itunes.apple.com/verifyReceipt"
SANDBOX_VALIDATION_URL = "https://sandbox.itunes.apple.com/verifyReceipt"
STATUS_SANDBOX_RECEIPT_ERROR = 21007

def __init__(
self, receipt_data, password=None, exclude_old_transactions=False,
Expand All @@ -51,88 +40,16 @@ def request_content(self):
request_content['password'] = self.password
return request_content

def verify_from(self, url, timeout=None, verify_ssl=True):
"""The actual implemention of verification request.
:func:`verify` calls this method to try to verifying for each servers.
:param str url: iTunes verification API URL.
:param float timeout: The value is connection timeout of the verifying
request. The default value is 30.0 when no `env` is given.
:param bool verify_ssl: SSL verification.
:return: :class:`itunesiap.receipt.Receipt` object if succeed.
:raises: Otherwise raise a request exception.
"""
post_body = json.dumps(self.request_content)
requests_post = requests.post
if self.proxy_url:
protocol = self.proxy_url.split('://')[0]
requests_post = functools.partial(requests_post, proxies={protocol: self.proxy_url})
if timeout is not None:
requests_post = functools.partial(requests_post, timeout=timeout)
try:
http_response = requests_post(url, post_body, verify=verify_ssl)
except requests.exceptions.RequestException as e:
raise exceptions.ItunesServerNotReachable(exc=e)

if http_response.status_code != 200:
raise exceptions.ItunesServerNotAvailable(http_response.status_code, http_response.content)

response = receipt.Response(json.loads(http_response.content.decode('utf-8')))
if response.status != 0:
raise exceptions.InvalidReceipt(response.status, response=response)
return response

def verify(self, **options):
"""Try verification with current environment.
If verify_ssl is true, Apple's SSL certificiate will be
verified. The verify_ssl is set to false by default for
backwards compatibility.
See also:
- Receipt_Validation_Programming_Guide_.
.. _Receipt_Validation_Programming_Guide: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
:param itunesiap.environment.Environment env: Override the environment.
:param float timeout: The value is connection timeout of the verifying
request. The default value is 30.0 when no `env` is given.
:param bool use_production: The value is weather verifying in
production server or not. The default value is :class:`bool` True
when no `env` is given.
:param bool use_sandbox: The value is weather verifying in
sandbox server or not. The default value is :class:`bool` False
when no `env` is given.
:param bool verify_ssl: The value is weather enabling SSL verification
or not. WARNING: DO NOT TURN IT OFF WITHOUT A PROPER REASON. IF YOU
DON'T UNDERSTAND WHAT IT MEANS, NEVER SET IT YOURSELF.
:return: :class:`itunesiap.receipt.Receipt` object if succeed.
:raises: Otherwise raise a request exception.
"""
env = options.get('env')
if not env: # backward compitibility
env = Environment._stack[-1]
use_production = options.get('use_production', env.use_production)
use_sandbox = options.get('use_sandbox', env.use_sandbox)
verify_ssl = options.get('verify_ssl', env.verify_ssl)
timeout = options.get('timeout', env.timeout)
assert(env.use_production or env.use_sandbox)

response = None
if use_production:
try:
response = self.verify_from(RECEIPT_PRODUCTION_VALIDATION_URL, timeout=timeout, verify_ssl=verify_ssl)
except exceptions.InvalidReceipt as e:
if not use_sandbox or e.status != STATUS_SANDBOX_RECEIPT_ERROR:
raise
class Request(RequestBase, RequestsVerify, AiohttpVerify):
"""Validation request with raw receipt.
if not response and use_sandbox:
try:
response = self.verify_from(RECEIPT_SANDBOX_VALIDATION_URL, timeout=timeout, verify_ssl=verify_ssl)
except exceptions.InvalidReceipt:
raise
Use `verify` method to try verification and get Receipt or exception.
For detail, see also the Apple document: `<https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html>`_.
return response
:param str receipt_data: An iTunes receipt data as Base64 encoded string.
:param str password: Only used for receipts that contain auto-renewable subscriptions. Your app's shared secret (a hexadecimal string).
:param bool exclude_old_transactions: Only used for iOS7 style app receipts that contain auto-renewable or non-renewing subscriptions. If value is true, response includes only the latest renewal transaction for any subscriptions.
:param proxy_url: A proxy url to access the iTunes validation url.
(It is an attribute of :func:`verify` but misplaced here)
"""
8 changes: 8 additions & 0 deletions itunesiap/shortcut.py
Expand Up @@ -50,3 +50,11 @@ def verify(
request = Request(
receipt_data, password, exclude_old_transactions, proxy_url=proxy_url)
return request.verify(**kwargs)


def aioverify(
receipt_data, password=None, exclude_old_transactions=False, **kwargs):
proxy_url = kwargs.pop('proxy_url', None)
request = Request(
receipt_data, password, exclude_old_transactions, proxy_url=proxy_url)
return request.aioverify(**kwargs)
71 changes: 71 additions & 0 deletions itunesiap/verify_aiohttp.py
@@ -0,0 +1,71 @@
import asyncio
import json
import aiohttp

from . import receipt
from . import exceptions
from .environment import default as default_env


class AiohttpVerify:

@asyncio.coroutine
def aioverify_from(self, url, timeout):
body = json.dumps(self.request_content).encode()
with aiohttp.ClientSession() as session:
try:
http_response = yield from session.post(url, data=body, timeout=timeout)
except asyncio.TimeoutError as e:
raise exceptions.ItunesServerNotReachable(exc=e)
if http_response.status != 200:
response_text = yield from http_response.text()
raise exceptions.ItunesServerNotAvailable(http_response.status, response_text)
response_body = yield from http_response.text()
response = receipt.Response(json.loads(response_body))
if response.status != 0:
raise exceptions.InvalidReceipt(response.status, response=response)
return response

@asyncio.coroutine
def aioverify(self, **options):
"""Try to verify the given receipt with current environment.
See also:
- Receipt_Validation_Programming_Guide_.
.. _Receipt_Validation_Programming_Guide: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
:param itunesiap.environment.Environment env: Override the environment.
:param float timeout: The value is connection timeout of the verifying
request. The default value is 30.0 when no `env` is given.
:param bool use_production: The value is weather verifying in
production server or not. The default value is :class:`bool` True
when no `env` is given.
:param bool use_sandbox: The value is weather verifying in
sandbox server or not. The default value is :class:`bool` False
when no `env` is given.
:param bool verify_ssl: The value will be ignored.
:return: :class:`itunesiap.receipt.Receipt` object if succeed.
:raises: Otherwise raise a request exception.
"""
env = options.get('env', default_env)
use_production = options.get('use_production', env.use_production)
use_sandbox = options.get('use_sandbox', env.use_sandbox)
verify_ssl = options.get('verify_ssl', env.verify_ssl) # noqa
timeout = options.get('timeout', env.timeout)

response = None
if use_production:
try:
response = yield from self.aioverify_from(self.PRODUCTION_VALIDATION_URL, timeout=timeout)
except exceptions.InvalidReceipt as e:
if not use_sandbox or e.status != self.STATUS_SANDBOX_RECEIPT_ERROR:
raise
if not response and use_sandbox:
try:
response = yield from self.aioverify_from(self.SANDBOX_VALIDATION_URL, timeout=timeout)
except exceptions.InvalidReceipt as e:
raise
return response
93 changes: 93 additions & 0 deletions itunesiap/verify_requests.py
@@ -0,0 +1,93 @@

import json
import functools
import requests

from . import receipt
from . import exceptions
from .environment import Environment


class RequestsVerify(object):
def verify_from(self, url, timeout=None, verify_ssl=True):
"""The actual implemention of verification request.
:func:`verify` calls this method to try to verifying for each servers.
:param str url: iTunes verification API URL.
:param float timeout: The value is connection timeout of the verifying
request. The default value is 30.0 when no `env` is given.
:param bool verify_ssl: SSL verification.
:return: :class:`itunesiap.receipt.Receipt` object if succeed.
:raises: Otherwise raise a request exception.
"""
post_body = json.dumps(self.request_content)
requests_post = requests.post
if self.proxy_url:
protocol = self.proxy_url.split('://')[0]
requests_post = functools.partial(requests_post, proxies={protocol: self.proxy_url})
if timeout is not None:
requests_post = functools.partial(requests_post, timeout=timeout)
try:
http_response = requests_post(url, post_body, verify=verify_ssl)
except requests.exceptions.RequestException as e:
raise exceptions.ItunesServerNotReachable(exc=e)

if http_response.status_code != 200:
raise exceptions.ItunesServerNotAvailable(http_response.status_code, http_response.content)

response = receipt.Response(json.loads(http_response.content.decode('utf-8')))
if response.status != 0:
raise exceptions.InvalidReceipt(response.status, response=response)
return response

def verify(self, **options):
"""Try verification with current environment.
See also:
- Receipt_Validation_Programming_Guide_.
.. _Receipt_Validation_Programming_Guide: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
:param itunesiap.environment.Environment env: Override the environment.
:param float timeout: The value is connection timeout of the verifying
request. The default value is 30.0 when no `env` is given.
:param bool use_production: The value is weather verifying in
production server or not. The default value is :class:`bool` True
when no `env` is given.
:param bool use_sandbox: The value is weather verifying in
sandbox server or not. The default value is :class:`bool` False
when no `env` is given.
:param bool verify_ssl: The value is weather enabling SSL verification
or not. WARNING: DO NOT TURN IT OFF WITHOUT A PROPER REASON. IF YOU
DON'T UNDERSTAND WHAT IT MEANS, NEVER SET IT YOURSELF.
:return: :class:`itunesiap.receipt.Receipt` object if succeed.
:raises: Otherwise raise a request exception.
"""
env = options.get('env')
if not env: # backward compitibility
env = Environment._stack[-1]
use_production = options.get('use_production', env.use_production)
use_sandbox = options.get('use_sandbox', env.use_sandbox)
verify_ssl = options.get('verify_ssl', env.verify_ssl)
timeout = options.get('timeout', env.timeout)
assert(env.use_production or env.use_sandbox)

response = None
if use_production:
try:
response = self.verify_from(self.PRODUCTION_VALIDATION_URL, timeout=timeout, verify_ssl=verify_ssl)
except exceptions.InvalidReceipt as e:
if not use_sandbox or e.status != self.STATUS_SANDBOX_RECEIPT_ERROR:
raise

if not response and use_sandbox:
try:
response = self.verify_from(self.SANDBOX_VALIDATION_URL, timeout=timeout, verify_ssl=verify_ssl)
except exceptions.InvalidReceipt:
raise

return response
17 changes: 13 additions & 4 deletions setup.py
@@ -1,5 +1,6 @@
from __future__ import with_statement

import sys
from setuptools import setup


Expand All @@ -16,10 +17,21 @@ def get_readme():
return ''


install_requires = [
'requests[security]', 'prettyexc>=0.6.0',
'six', 'pytz', 'python-dateutil',
]
tests_require = [
'pytest>=3.0.0', 'pytest-cov', 'tox', 'mock', 'patch',
]

if sys.version_info[:2] >= (3, 4):
install_requires.extend([
'aiohttp', 'aiodns',
])
tests_require.extend([
'pytest-asyncio==0.5.0'
])

setup(
name='itunes-iap',
Expand All @@ -35,10 +47,7 @@ def get_readme():
package_data={
'itunesiap': ['version.txt']
},
install_requires=[
'requests[security]', 'prettyexc>=0.6.0',
'six', 'pytz', 'python-dateutil',
],
install_requires=install_requires,
tests_require=tests_require,
extras_require={
'tests': tests_require,
Expand Down

0 comments on commit d47552e

Please sign in to comment.