From 0adea2c79c75aa00caa46cd53ffafa0247277c0d Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 23 Jul 2017 01:34:12 +0900 Subject: [PATCH] Documentation --- .travis.yml | 3 +- README.rst | 69 +++++++-------- docs/conf.py | 183 +++++++++++++++++++++++++++++++++++++++ docs/environment.rst | 14 +++ docs/index.rst | 25 ++++++ docs/quick.rst | 68 +++++++++++++++ docs/receipt.rst | 17 ++++ docs/request.rst | 14 +++ itunesiap/environment.py | 43 ++++++++- itunesiap/exceptions.py | 5 +- itunesiap/receipt.py | 5 ++ itunesiap/request.py | 38 ++++++-- itunesiap/shortcut.py | 45 ++++++++-- setup.py | 1 + 14 files changed, 470 insertions(+), 60 deletions(-) create mode 100644 docs/conf.py create mode 100644 docs/environment.rst create mode 100644 docs/index.rst create mode 100644 docs/quick.rst create mode 100644 docs/receipt.rst create mode 100644 docs/request.rst diff --git a/.travis.yml b/.travis.yml index 0393405..c524add 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,11 +12,12 @@ python: # command to install dependencies install: - "pip install --upgrade pip" - - "pip install flake8 python-coveralls '.[tests]'" + - "pip install flake8 python-coveralls sphinx '.[tests]'" # command to run tests script: - "flake8 --ignore=E501 ." - "pytest --cov=itunesiap -vv tests/" + - "python -msphinx -M html docs build" after_success: - "coveralls" - bash <(curl -s https://codecov.io/bash) || echo "Codecov did not collect coverage reports" diff --git a/README.rst b/README.rst index 56506eb..1e8a3be 100644 --- a/README.rst +++ b/README.rst @@ -6,18 +6,10 @@ itunes-iap v2 .. image:: https://coveralls.io/repos/github/youknowone/itunes-iap/badge.svg?branch=master :target: https://coveralls.io/github/youknowone/itunes-iap?branch=master -Note for v1 users +The quick example ----------------- -There was breaking changes between v1 and v2 APIs. - -- Specify version `0.6.6` for latest v1 API when you don't need new APIs. -- Or use `import itunesiap.legacy as itunesiap` instead of `import itunesiap`. (`from itunesiap import xxx` to `from itunesiap.legacy import xxx`) - -Quick example -------------- - -Create request to create a request to itunes verify api. +Create request to create a request to itunes verifying api. .. sourcecode:: python @@ -28,53 +20,50 @@ Create request to create a request to itunes verify api. >>> print('invalid receipt') >>> print response.receipt.last_in_app.product_id # other values are also available as property! -Practical values are: product_id, original_transaction_id, quantity, unique_identifier +Practically useful attributes are: + `product_id`, `original_transaction_id`, `quantity` and `unique_identifier`. -Quick example with password (Apple Shared Secret) -------------------------------------------------- +See the full document in: + - :func:`itunesiap.verify`: The verifying function. + - :class:`itunesiap.receipt.InApp`: The receipt object. -Create request to create a request to itunes verify api. -.. sourcecode:: python +Installation +------------ - >>> import itunesiap - >>> try: - >>> response = itunesiap.verify(raw_data, password) # Just add password - >>> except itunesiap.exc.InvalidReceipt as e: - >>> print('invalid receipt') - >>> in_app = response.receipt.last_in_app # Get the latest receipt returned by Apple +PyPI is the recommended way. +.. sourcecode:: shell -Verification policy -------------------- + $ pip install itunesiap -Set verification mode for production or sandbox api. Review mode also available for appstore review. -.. sourcecode:: python +Apple in-review mode +-------------------- - >>> import itunesiap - >>> # `review` enables both production and sandbox for appstore review. `production`, `sandbox` or `review` is available. - >>> response = request.verify(raw_data, env=itunesiap.env.review) +In review mode, your actual users who use older versions want to verify in +production server but the reviewers in Apple office want to verify in sandbox +server. + +Note: The default env is `production` mode which doesn't allow any sandbox +verifications. -Or +You can change the verifying mode by specifying `env`. .. sourcecode:: python - >>> import itunesiap - >>> response = itunesiap.verify(raw_data, use_sandbox=True): # additional change for current environment. + >>> # review mode + >>> itunesiap.verify(raw_data, env=itunesiap.env.review) -Proxy ------ -Put `proxy_url` for proxies. +Note for v1 users +----------------- -.. sourcecode:: python +There was breaking changes between v1 and v2 APIs. + +- Specify version `0.6.6` for latest v1 API when you don't need new APIs. +- Or use `import itunesiap.legacy as itunesiap` instead of `import itunesiap`. (`from itunesiap import xxx` to `from itunesiap.legacy import xxx`) - >>> import itunesiap - >>> try: - >>> response = itunesiap.verify(raw_data, proxy_url='https://your.proxy.url/') - >>> except itunesiap.exc.InvalidReceipt as e: - >>> ... Contributors ------------ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..0b15ec8 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +def get_version(): + with open('../itunesiap/version.txt') as f: + return f.read().strip() + +# +# itunes-iap documentation build configuration file, created by +# sphinx-quickstart on Sat Jul 22 17:34:06 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.coverage'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'itunes-iap' +copyright = '2017, Jeong YunWon' +author = 'Jeong YunWon' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = get_version() +# The full version, including alpha/beta/rc tags. +release = get_version() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + 'donate.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'itunesiapdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'itunes-iap.tex', 'itunes-iap Documentation', + 'Jeong YunWon', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'itunes-iap', 'itunes-iap Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'itunes-iap', 'itunes-iap Documentation', + author, 'itunes-iap', 'One line description of project.', + 'Miscellaneous'), +] + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/docs/environment.rst b/docs/environment.rst new file mode 100644 index 0000000..7119aea --- /dev/null +++ b/docs/environment.rst @@ -0,0 +1,14 @@ +Environment +=========== + +.. automodule:: itunesiap.environment + + +.. autoclass:: itunesiap.environment.Environment + :members: + +.. autodata:: itunesiap.environment.default +.. autodata:: itunesiap.environment.production +.. autodata:: itunesiap.environment.sandbox +.. autodata:: itunesiap.environment.review +.. autodata:: itunesiap.environment.unsafe diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..4613009 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,25 @@ +.. itunes-iap documentation master file, created by + sphinx-quickstart on Sat Jul 22 17:34:06 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +iTunes In-App purchase verification in Python +============================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + quick.rst + request.rst + receipt.rst + environment.rst + +.. include:: ../README.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/quick.rst b/docs/quick.rst new file mode 100644 index 0000000..6366538 --- /dev/null +++ b/docs/quick.rst @@ -0,0 +1,68 @@ +The quick guide +=============== + +Create request to create a request to itunes verify api. + +.. sourcecode:: python + + >>> import itunesiap + >>> try: + >>> response = itunesiap.verify(raw_data) # base64-encoded data + >>> except itunesiap.exc.InvalidReceipt as e: + >>> print('invalid receipt') + >>> print response.receipt.last_in_app.product_id + >>> # other values are also available as properties! + +Practically useful attributes are: `product_id`, `original_transaction_id`, `quantity` and `unique_identifier`. +See the full document in :class:`itunesiap.receipt.InApp`. + +Note that most of the use cases are covered by the :func:`itunesiap.verify` function. + +.. autofunction:: itunesiap.verify + +Apple in-review mode +-------------------- + +In review mode, your actual users who use older versions want to verify in +production server but the reviewers in Apple office want to verify in sandbox +server. + +Note: The default env is `production` mode which doesn't allow any sandbox +verifications. + +You can change the verifying mode by specifying `env`. + +.. sourcecode:: python + + >>> # review mode + >>> itunesiap.verify(raw_data, env=itunesiap.env.review) + >>> # sandbox mode + >>> itunesiap.verify(raw_data, env=itunesiap.env.sandbox) + +Also directly passing arguments are accepted: + +.. sourcecode:: python + + >>> # review mode + >>> itunesiap.verify(raw_data, use_production=True, use_sandbox=True) + + +Password for shared secret +-------------------------- + +When you have shared secret for your app, the verifying process requires a +shared secret password. + +About the shared secret, See: In-App_Purchase_Configuration_Guide_. + +.. sourcecode:: python + + >>> try: + >>> # Add password as a parameter + >>> response = itunesiap.verify(raw_data, password=password) + >>> except itunesiap.exc.InvalidReceipt as e: + >>> print('invalid receipt') + >>> in_app = response.receipt.last_in_app # Get the latest receipt returned by Apple + + +.. _In-App_Purchase_Configuration_Guide: https://developer.apple.com/library/content/documentation/LanguagesUtilities/Conceptual/iTunesConnectInAppPurchase_Guide/Chapters/CreatingInAppPurchaseProducts.html \ No newline at end of file diff --git a/docs/receipt.rst b/docs/receipt.rst new file mode 100644 index 0000000..957c561 --- /dev/null +++ b/docs/receipt.rst @@ -0,0 +1,17 @@ +Receipt +======= + +.. automodule:: itunesiap.receipt + +.. autoclass:: itunesiap.receipt.ObjectMapper + :members: + +.. autoclass:: itunesiap.receipt.Response + :members: + +.. autoclass:: itunesiap.receipt.Receipt + :members: + +.. autoclass:: itunesiap.receipt.InApp + :members: + diff --git a/docs/request.rst b/docs/request.rst new file mode 100644 index 0000000..2dc461b --- /dev/null +++ b/docs/request.rst @@ -0,0 +1,14 @@ +Request +======= + +.. automodule:: itunesiap.request + +.. autoclass:: itunesiap.request.Request + :members: + +.. automodule:: itunesiap.exceptions + +.. autoclass:: itunesiap.exceptions.ItunesServerNotAvailable + +.. autoclass:: itunesiap.exceptions.ItunesServerNotReachable +.. autoclass:: itunesiap.exceptions.InvalidReceipt diff --git a/itunesiap/environment.py b/itunesiap/environment.py index 81d31a1..2bf15a0 100644 --- a/itunesiap/environment.py +++ b/itunesiap/environment.py @@ -11,18 +11,29 @@ def push(self, env): class Environment(object): - """Environment provides option preset for `Request`. `default` is default""" + """Environment provides option preset for `Request`. `default` is default. - ITEMS = ('use_production', 'use_sandbox') + By passing an environment object to :func:`itunesiap.verify` or + :func:`itunesiap.request.Request.verify` function, it replaces verifying + policies. + """ + + ITEMS = ( + 'use_production', 'use_sandbox', 'timeout', 'exclude_old_transactions', + 'verify_ssl') def __init__(self, **kwargs): self.use_production = kwargs.get('use_production', True) self.use_sandbox = kwargs.get('use_sandbox', False) self.timeout = kwargs.get('timeout', None) - self.verify_ssl = kwargs.get('verify_ssl', True) self.exclude_old_transactions = kwargs.get('exclude_old_transactions', False) + self.verify_ssl = kwargs.get('verify_ssl', True) + + def __repr__(self): + return u'<{self.__class__.__name__} use_production={self.use_production} use_sandbox={self.use_sandbox} timeout={self.timeout} exclude_old_transactions={self.exclude_old_transactions} verify_ssl={self.verify_ssl}>'.format(self=self) def clone(self, **kwargs): + """Clone the environment with additional parameter override""" options = self.extract() options.update(**kwargs) return self.__class__(**options) @@ -65,15 +76,39 @@ def current(cls): return cls._stack[-1] +#: Use only production server with 30 seconds of timeout. default = Environment(use_production=True, use_sandbox=False, timeout=30.0, verify_ssl=True) +#: Use only production server with 30 seconds of timeout. production = Environment(use_production=True, use_sandbox=False, timeout=30.0, verify_ssl=True) +#: Use only sandbox server with 30 seconds of timeout. sandbox = Environment(use_production=False, use_sandbox=True, timeout=30.0, verify_ssl=True) + review = Environment(use_production=True, use_sandbox=True, timeout=30.0, verify_ssl=True) +'''Use both production and sandbox servers with 30 seconds of timeout. + +Try to verify in production server first and fall back to the sandbox server. +This is useful when your server is being used both for real users and Apple +reviewers. +Using review mode for a real service is possible, but be awared: it is not +100% safe. Your testers can getting advantage of free IAP in production +version. +A rough solution what I suggest is: + +.. sourcecode:: python + + >>> if client_version == review_version: + >>> env = itunesiap.env.review + >>> else: + >>> env = itunesiap.env.production + >>> + >>> itunesiap.verify(receipt, env=env) + +''' unsafe = Environment(use_production=True, use_sandbox=True, verify_ssl=False) -Environment._stack.push(default) # for backward compatibility +Environment._stack.append(default) # for backward compatibility @deprecated diff --git a/itunesiap/exceptions.py b/itunesiap/exceptions.py index bf74a89..318dd29 100644 --- a/itunesiap/exceptions.py +++ b/itunesiap/exceptions.py @@ -7,14 +7,15 @@ class RequestError(E): class ItunesServerNotAvailable(RequestError): - pass + '''iTunes server is not available. No response.''' class ItunesServerNotReachable(ItunesServerNotAvailable): - pass + '''iTunes server is not reachable - including connection timeout.''' class InvalidReceipt(RequestError): + '''A receipt was given by iTunes server but it has error.''' _req_kwargs_keys = ['status'] _descriptions = { 21000: 'The App Store could not read the JSON object you provided.', diff --git a/itunesiap/receipt.py b/itunesiap/receipt.py index 140c69d..5e7c669 100644 --- a/itunesiap/receipt.py +++ b/itunesiap/receipt.py @@ -1,4 +1,9 @@ +''':mod:`itunesiap.receipt` +Response and Receipt module. + + +''' import pytz import dateutil.parser import warnings diff --git a/itunesiap/request.py b/itunesiap/request.py index b3fbc0b..c527d9c 100644 --- a/itunesiap/request.py +++ b/itunesiap/request.py @@ -17,9 +17,13 @@ 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: ``_. :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) """ def __init__( @@ -29,13 +33,17 @@ def __init__( self.password = password self.exclude_old_transactions = exclude_old_transactions self.proxy_url = kwargs.pop('proxy_url', None) + if kwargs: # pragma: no cover + raise TypeError( + u"__init__ got unexpected keyword argument {}".format( + ', '.join(kwargs.keys()))) def __repr__(self): return u''.format(self.receipt_data[:20]) @property def request_content(self): - """Build request body for iTunes.""" + """Instantly built request body for iTunes.""" request_content = { 'receipt-data': self.receipt_data, 'exclude-old-transactions': self.exclude_old_transactions} @@ -44,9 +52,13 @@ def request_content(self): return request_content def verify_from(self, url, timeout=None, verify_ssl=True): - """Try verification from given url. + """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. @@ -78,10 +90,24 @@ def verify(self, **options): verified. The verify_ssl is set to false by default for backwards compatibility. - :param Environment env: Override environment if given - :param bool use_production: Override environment value if given - :param bool use_sandbox: Override environment value if given - :param bool verify_ssl: Override environment value if given + 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. diff --git a/itunesiap/shortcut.py b/itunesiap/shortcut.py index a9d506f..b8fa503 100644 --- a/itunesiap/shortcut.py +++ b/itunesiap/shortcut.py @@ -4,16 +4,47 @@ def verify( receipt_data, password=None, exclude_old_transactions=False, **kwargs): - """Shortcut API for :class:`itunesiap.request.Request` + """Shortcut API for :class:`itunesiap.request.Request`. - :param str receipt_data: An iTunes receipt data as Base64 encoded string. - :param proxy_url: A proxy url to access the iTunes validation url - :param bool use_production: Override environment value if given - :param bool use_sandbox: Override environment value if given - :param bool verify_ssl: Override environment value if given + See also: + - Receipt_Validation_Programming_Guide_. + + .. _Receipt_Validation_Programming_Guide: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html + + :param str receipt_data: :class:`itunesiap.request.Request` variable. + An iTunes receipt data as Base64 encoded string. + :param str password: :class:`itunesiap.request.Request` variable. Optional. + Only used for receipts that contain auto-renewable subscriptions. Your + app's shared secret (a hexadecimal string). + :param bool exclude_old_transactions: :class:`itunesiap.request.Request` + variable. Optional. 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 itunesiap.environment.Environment env: Set base environment value. + See :mod:`itunesiap.environment` for detail. + :param float timeout: :func:`itunesiap.request.Request.verify` variable. + Keyword-only optional. The value is connection timeout of the verifying + request. The default value is 30.0 when no `env` is given. + :param bool use_production: :func:`itunesiap.request.Request.verify` + variable. Keyword-only optional. 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: :func:`itunesiap.request.Request.verify` + variable. Keyword-only optional. 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: :func:`itunesiap.request.Request.verify` variable. + Keyword-only optional. 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. + :param str proxy_url: Keyword-only optional. A proxy url to access the + iTunes validation url. :return: :class:`itunesiap.receipt.Receipt` object if succeed. - :raises: Otherwise raise a request exception. + :raises: Otherwise raise a request exception in :mod:`itunesiap.exceptions`. """ proxy_url = kwargs.pop('proxy_url', None) request = Request( diff --git a/setup.py b/setup.py index 679c16e..541b844 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ def get_readme(): tests_require=tests_require, extras_require={ 'tests': tests_require, + 'doc': ['sphinx'], }, classifiers=[ 'Intended Audience :: Developers',