diff --git a/.editorconfig b/.editorconfig index 5daf8b75..27bfc856 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,6 +3,7 @@ root = true [*] end_of_line = lf insert_final_newline = true +max_line_length = 120 [Makefile] indent_style = tab diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..a6fb5b1e --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = E203, E266, E501, E722, W503 +max-line-length = 80 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +exclude = build,docs diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 00000000..8ffa95b1 --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,65 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: + push: + branches: [ master ] + paths-ignore: + - '**.md' + - 'apidoc/**' + - 'doc/**' + pull_request: + branches: [ master ] + paths-ignore: + - '**.md' + - 'apidoc/**' + - 'doc/**' + +jobs: + lint: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Lint with flake8 + run: | + pip install flake8 flake8-bugbear + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. + flake8 . --count --exit-zero --statistics + + test: + runs-on: ubuntu-20.04 + strategy: + max-parallel: 1 + matrix: + python-version: [3.6, 3.7, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -e .'[test]' + - name: Run tests with ${{ matrix.python-version }} + env: + APP_ID: u5xB92MjVH94kH6p3M66DUua-MdYXbMMI + APP_KEY: ${{ secrets.APP_KEY }} + MASTER_KEY: ${{ secrets.MASTER_KEY }} + USE_REGION: US + run: + nosetests -v diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 00000000..8df82c7a --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,31 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.gitignore b/.gitignore index eec99d58..a25fe226 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] +*$py.class # C extensions *.so @@ -13,6 +14,7 @@ develop-eggs/ dist/ downloads/ eggs/ +.eggs/ lib/ lib64/ parts/ @@ -21,10 +23,7 @@ var/ *.egg-info/ .installed.cfg *.egg -bin/ -include/ -man/ -pip-selfcheck.json + # PyInstaller # Usually these files are written by a python script from a template @@ -40,9 +39,12 @@ pip-delete-this-directory.txt htmlcov/ .tox/ .coverage +.coverage.* .cache nosetests.xml coverage.xml +*,cover +.hypothesis/ # Translations *.mo @@ -50,13 +52,49 @@ coverage.xml # Django stuff: *.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy # Sphinx documentation -docs/_build/ +apidoc/_build/ # PyBuilder target/ -# Editor -*.swp -.idea/ +# IPython Notebook +.ipynb_checkpoints + +# Mypy +.mypy_cache/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# LeanCloud settings +.leancloud/ + +# IDE-config +.idea +tags diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/.pyup.yml b/.pyup.yml new file mode 100644 index 00000000..30182e6b --- /dev/null +++ b/.pyup.yml @@ -0,0 +1,4 @@ +# autogenerated pyup.io config file +# see https://pyup.io/docs/configuration/ for all available options + +schedule: every month diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fe02380e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: python - -matrix: - include: - - python: "2.7" - env: - - APP_ID=8FfQwpvihLHK4htqmtEvkNrv - - APP_KEY=eE9tNOcCiWoMHM1phxY41rAz - - MASTER_KEY=75zAjEJSj7lifKQqKSTryae9 - - USE_REGION=US - - python: "3.5" - env: - - APP_ID=AjQYwoIyObTeEkD16v1eCq55 - - APP_KEY=AJpoJrqy1aliyXvcs0SwWrsy - - MASTER_KEY=Uk6DT2Mc2kCACvLyi3PU60p3 - - USE_REGION=US - -sudo: false - -install: - - pip install -r dev-requirements.txt - - pip install codecov - -script: - - nosetests --with-coverage --cover-package=leancloud - -after_success: - - codecov - -notifications: - webhooks: https://hook.bearychat.com/=bw52Y/travis/a6614ed1ce835ba1f88a78bd1810a51b diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..65c5ca88 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..d6dec2d3 --- /dev/null +++ b/Pipfile @@ -0,0 +1,30 @@ +[[source]] + +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + + +[dev-packages] + +wsgi-intercept = "*" +nose = "*" +sphinx-rtd-theme = "*" +flask = "*" + + +[packages] + +arrow = "*" +"iso8601" = "*" +six = ">=1.11.0" +qiniu = ">=7.1.4,<7.2.4" +"urllib3" = ">=1.24.3,<=1.25.3" +requests = ">=2.20.0,<=2.22.0" +requests-toolbelt = ">=1.0.0" +Werkzeug = ">=0.11.11,<1.0.0" +gevent = ">=22.10.2,<23.0.0" +typing = { version = "*", markers = "python_version < '3.5.0'" } +pyopenssl = { version = "*", markers = "python_version < '2.7.9'" } +idna = { version = "*", markers = "python_version < '2.7.9'" } +markupsafe = "<=2.0.1" diff --git a/README.md b/README.md index 3de1b5b1..3e5cd1e1 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,96 @@ # Python-SDK -[![Build Status](https://travis-ci.org/leancloud/python-sdk.svg?branch=master)](https://travis-ci.org/leancloud/python-sdk) +![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/leancloud/python-sdk/Python%20package/master) +[![Codecov](https://img.shields.io/codecov/c/github/leancloud/python-sdk.svg)](https://codecov.io/gh/leancloud/python-sdk) -LeanCloud Python SDK (under development) +LeanCloud Python SDK ## Install ```bash -pip install leancloud-sdk +pip install leancloud ``` or ``` -easy_install leancloud-sdk +easy_install leancloud ``` -Maybe you need the `sudo` prefix depends on you OS environment. +Maybe you need the `sudo` prefix depends on your OS environment. + +## Supported Python Versions + +Python 2.7.18 and Python 3.6+. ## Generate API document -```bash -cd docs +Install dependencies: + +```sh +pip install Sphinx sphinx_rtd_theme +``` + +```sh +cd apidoc make html ``` +## Run Tests + +Configure the following environment variables: + +- `APP_ID` +- `APP_KEY` +- `MASTER_KEY` +- `USE_REGION` + +Make sure the following options are configured on the LeanCloud console: + +- Data Storage > Settings > Include ACL with objects being queried: **checked** +- Push Notification > Push notification settings > Prevent clients from sending push notifications: **unchecked** +- Settings > Security > Service switches > Push notifications: **enabled** +- Settings > Security > Service switches > SMS: **disabled** + +And there is a cloud function naming `add` which returns `3` for `add(a=1, b=2)` deployed on the LeanEngine production environment of the application. +For example: + +```js +AV.Cloud.define('add', async function (request) { + return request.params["a"] + request.params["b"] +}) +``` + +Install dependencies: + +```sh +pip install -e .'[test]' +``` + +Run tests: + +```sh +python -m nose +``` + +Run a single test without swallowing print: + +```sh +python -m nose -v --nocapture tests/test_engine.py:test_lean_engine_error +``` + +## Linter and Formatter + +Currently, flake8 (linter) and black (formatter) are used. +But we are still exploring. + +## Release a New Version + +0. Edit `changelog` and `setup.py` (`version`). +1. Generate API doc. +2. Commit the changes above and send a pull request. +3. The maintainer will review and merge the pull request, then create a new release at GitHub web UI. +4. A new version of the package will be published to PyPI automatically (via GitHub Actions). ## License diff --git a/docs/Makefile b/apidoc/Makefile similarity index 98% rename from docs/Makefile rename to apidoc/Makefile index 195d48da..6f0d6e8f 100644 --- a/docs/Makefile +++ b/apidoc/Makefile @@ -53,8 +53,11 @@ clean: html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @rm -rf ../docs + @mv $(BUILDDIR)/html ../docs + @touch ../docs/.nojekyll @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + @echo "Build finished. The HTML pages are in ../docs." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml diff --git a/docs/conf.py b/apidoc/conf.py similarity index 74% rename from docs/conf.py rename to apidoc/conf.py index b6ef664b..d726d93a 100644 --- a/docs/conf.py +++ b/apidoc/conf.py @@ -12,95 +12,95 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os -import shlex +import sys # 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. -sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath("../")) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# 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.autodoc", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +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' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'LeanCloud-Python-SDK' -copyright = u'2015, asaka' -author = u'asaka' +project = u"LeanCloud-Python-SDK" +copyright = u"2015, asaka" +author = u"asaka" # 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 = '1.0.0' +version = "2.6" # The full version, including alpha/beta/rc tags. -release = '1.0.0' +release = "2.6.1" # 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 = 'en_US' +language = "zh_CN" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build", "docs"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -110,143 +110,146 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = "sphinx_rtd_theme" + # 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 = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # 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'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'LeanCloud-Python-SDKdoc' +htmlhelp_basename = "LeanCloud-Python-SDKdoc" # -- 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', + # 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, 'LeanCloud-Python-SDK.tex', u'LeanCloud-Python-SDK Documentation', - u'asaka', 'manual'), + ( + master_doc, + "LeanCloud-Python-SDK.tex", + u"LeanCloud-Python-SDK Documentation", + u"asaka", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -254,12 +257,17 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'leancloud-python-sdk', u'LeanCloud-Python-SDK Documentation', - [author], 1) + ( + master_doc, + "leancloud-python-sdk", + u"LeanCloud-Python-SDK Documentation", + [author], + 1, + ) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -268,19 +276,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'LeanCloud-Python-SDK', u'LeanCloud-Python-SDK Documentation', - author, 'LeanCloud-Python-SDK', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "LeanCloud-Python-SDK", + u"LeanCloud-Python-SDK Documentation", + author, + "LeanCloud-Python-SDK", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/apidoc/index.rst b/apidoc/index.rst new file mode 100644 index 00000000..3af9bd8f --- /dev/null +++ b/apidoc/index.rst @@ -0,0 +1,151 @@ +================================================ +LeanCloud-Python-SDK API 文档 +================================================ + +.. toctree:: + :maxdepth: 4 + +leancloud +========= + +.. autofunction:: leancloud.init + +.. autofunction:: leancloud.use_master_key + +.. autofunction:: leancloud.use_production + +.. autofunction:: leancloud.use_region + +.. autoclass:: leancloud.FriendshipQuery + :show-inheritance: + :members: + :undoc-members: + +.. autoclass:: leancloud.LeanCloudError + :show-inheritance: + :members: + :undoc-members: + +.. autoclass:: leancloud.LeanCloudWarning + :show-inheritance: + :members: + :undoc-members: + +Object +------ + +.. autoclass:: leancloud.Object + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +User +---- + +.. autoclass:: leancloud.User + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +File +---- + +.. autoclass:: leancloud.File + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +Query +----- +.. autoclass:: leancloud.Query + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +Relation +-------- + +.. autoclass:: leancloud.Relation + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +Role +---- + +.. autoclass:: leancloud.Role + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +ACL +--- + +.. autoclass:: leancloud.ACL + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + + +GeoPoint +-------- + +.. autoclass:: leancloud.GeoPoint + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +Engine +------ + +.. autoclass:: leancloud.Engine + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +HttpsRedirectMiddleware +----------------------- + +.. autoclass:: leancloud.engine.HttpsRedirectMiddleware + :show-inheritance: + :members: + :undoc-members: + +CookieSessionMiddleware +----------------------- + +.. autoclass:: leancloud.engine.CookieSessionMiddleware + :show-inheritance: + :members: + :undoc-members: + +leancloud.push +============== + +.. automodule:: leancloud.push + :members: + :undoc-members: + :show-inheritance: + +leancloud.cloud +=================== + +.. automodule:: leancloud.cloud + :members: + :undoc-members: + :show-inheritance: + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/docs/make.bat b/apidoc/make.bat similarity index 100% rename from docs/make.bat rename to apidoc/make.bat diff --git a/changelog b/changelog index cd9739e5..c4b35612 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,320 @@ +## [3.0.2] - 2024-07-03 + +## Chore + +- Pin requests/urllib version on Python 3.7+ + +## [3.0.1] - 2024-06-25 + +## Fixed + +- Enable TCP keepalive socket option to fix "Connection reset by peer" problem. https://github.com/psf/requests/issues/4664 + +## [3.0.0] - 2024-04-08 + +## Change + +- Drop support for python 2.7 and 3.5 + +## Fixed + +- Require phone_number when verify sms code +- Pinned urllib3 to 1.x + +## [2.9.12] - 2022-11-24 + +## Fixed + +- Upgrade gevent version to support Python 3.11 + +## [2.9.11] - 2022-06-23 + +## Fixed + +- Fixed a typo in the API path used by `leancloud.User#verify_mobile_phone_number`. +- Pinned MarkupSafe to 2.0.1 or earlier. + +## [2.9.10] - 2022-01-28 + +## Fixed + +- Updated API domains for apps in the China North region. + +## [2.9.9] - 2021-12-14 + +### Added + +- LCFile exposes the `key` attribute for non-external files. + +### Fixed + +- created_at and updated_at were missing on LCFile. +- updated_at was missing on new created LCObject. + +## [2.9.8] - 2021-12-10 + +### Fixed + +- `url` was missing in included LCFile attribute + +## [2.9.7] - 2021-10-11 + +### Fixed +- onAuthData hook +- hide LCFile.url when failed to upload file + +## [2.9.6] - 2021-09-15 + +### Added +- onAuthData hook. + +## [2.9.5] - 2021-08-20 +### Fixed +- Forgot to send hook key in request header. + +## [2.9.4] - 2021-07-13 +### Fixed +- update qiniu sdk to fix parameter mismatch exception (#521) + +Thank berry_shanghai for bringing this issue to our attention. + +## [2.9.3] - 2021-05-19 +### Fixed +- dependency issues with werkzeug >= 2.0.0 + +## [2.9.2] - 2021-03-05 +### Fixed +- Python packaging issue causing installation failure on Python 2.7. + +## [2.9.1] - 2021-03-03 +### Changed +- Update dependencies: really support Python 3.9 and drop support for Python 3.5. + +## [2.9.0] - 2020-11-26 +### Added +- Allow specifying uploaded file path with MasterKey. +- Add Python 3.9 in CI tests. + +## [2.8.1] - 2020-10-21 +### Changed +- Remove deprecated internal interface invocation when saving external files. + +## [2.8.0] - 2020-07-15 +### Added +- `request_change_phone_number` and `change_phone_number` to verify mobile number *before* updating it. +- `request_sms_code` now accepts phone number in E.164 format. + +## [2.7.1] - 2020-06-18 +### Fixed +- LeanEngineError now encodes valid HTTP status code. + Previously it reused the `code` parameter, which would result in an invalid HTTP status code encoded, + if the value of the `code` parameter is not a valid HTTP status code. + You can also use the new status parameter to specify the HTTP status code now. + +## [2.7.0] - 2020-05-21 +### Added +- `_messageUpdate` hook +- `push.send` supports `prod` parameter + +## [2.6.1] - 2020-05-06 +### Fixed +- HttpsRedirectMiddleware now works with projects not using cloud functions +- regexp escaping issues + +## [2.6.0] - 2020-04-09 +### Added +- `_conversationAdded` and `_conversationRemoved` hooks + +## [2.5.1] - 2020-03-10 +### Fixed +- dependency issues with werkzeug + +## [2.5.0] - 2019-12-30 +### Added +- _rtmClientSign hook + +## [2.4.0] - 2019-12-11 +### Added +- creating conversations supports `is_unique` + +## [2.3.0] - 2019-12-02 +### Added +- support for Android Key (`LEANCLOUD_APP_ANDX_KEY`) in LeanEngine +- push.send: new param `flow_control` + +## [2.2.0] - 2019-10-24 +### Added +- _clientOnline and _clientOffline hooks +### Fixed +- fix _merge_metadata by @jinke18 +- typo in object_.get parameter + +## [2.1.14] - 2019-07-05 +### Fixed +- dependency issues with requests and werkzeug +- unable to request api for applications hosted in US node + +## [2.1.13] - 2019-06-19 +### Fixed +- dependency issues with requests and urllib3 + +## [2.1.12] - 2019-05-24 +### Fixed +- Push: expiration_time +- Push: updated_keys +- LeanEngine: X-LC-Prod request header + +## [2.1.11] - 2019-05-10 +### Fixed +- Error -1 when uploading large files to Qiniu +- TypeError in py3k when uploading files to Qiniu using BytesIO / StringIO +- Upgraded urlib3 to 1.24.3 (CVE-2019-11324) +### Added +- email argument for User.login + +## [2.1.10] - 2019-03-18 +### Changed +- Upgrade Qiniu SDK + +## [2.1.9] - 2019-03-11 +### Fixed +- Fixed CookieSessionMiddleware.pre_process fail when session_token doesn't exist in Cookies +- Fixed unit test for AVFile +### Added +- LeanCloud API App Router v2 for us-w1 and cn-e1 + +## [2.1.8] - 2017-04-09 +### Fixed +- Fixed requestSmsCode fail when sending international sms + +## [2.1.7] - 2017-12-27 +### Fixed +- Minor crash bug introduced by 2.1.6 fix + +## [2.1.6] - 2017-12-27 +### Fixed +- leancloud.File should accept any file like object + +## [2.1.5] - 2017-10-27 +### Fixed +- add dependency for `six` + +## [2.1.4] - 2017-10-13 +### Fixed +- reset_password_by_sms_code now works properly, thanks rickyfunfun<151266405@qq.com> for the patch + +## [2.1.3] - 2017-09-26 +### Fixed +- another unicode encoding error in python2 on CookieSessionMiddleware + +## [2.1.2] - 2017-09-26 +### Fixed +- unicode encoding error in python2 on CookieSessionMiddleware + +## [2.1.1] - 2017-09-22 +### Fixed +- unicode encoding error in python2 on leanengine + +## [2.1.0] - 2017-09-21 +### Added +- leancloud.Status class +- leancloud.SysMessage class +- allow query realtime message +- allow query with ACL +### Changed +- rename leancloud.cloudfunc to leancloud.cloud + +## [2.0.0] - 2017-08-05 +### Added +- leancloud.User.session_token property +### Fixed +- invalid timezone on leancloud.Object#created_at and leancloud.Object#updated_at +- leancloud.Query#get using get ACL permission, not find +### Removed +- leancloud.Engine#current_user (use leancloud.Engine#current.user instead) +- type_ param in leancloud.File constructor (use mime_type instead) +- type_ param in leancloud.File.create_with_url (use mime_type instead) +- leancloud.Object#fetch_when_save property (use fetch_when_save param in leancloud.Object#save function instead) +- leancloud.Object#attribute property +- leancloud.Relation#query not is a property, not a function +- leancloud.Query#does_not_exists (use leancloud.Query#does_not_exist instead) +- leancloud.Query#matched_key_in_query (use leancloud.Query#matches_key_in_query instead) + +## [1.13.0] - 2017-08-04 +### Added +- leancloud.Engine#wrap +- leancloud.Engine#register +- support leanengine hook key +- add fetch_user param in leancloud.Engine constructor +- support setting expires and max_age in CookieSessionMiddleware + +## [1.12.0] - 2017-06-05 +### Added +- Captcha related API +- Conversation related API +- support SNI in earlier python versions + +## [1.11.0] - 2017-04-14 +### Added +- LeanCloud API app router v2 + +## [1.10.0] - 2017-02-22 +### Added +- leancloud.Query#size_equal_to +- leancloud.Query#scan +- allow query leancloud.File +### Fixed +- cookie session middleware typo + +## [1.9.0] - 2016-12-13 +### Added +- leancloud.Object#save now accept fetch_when_save as keyword parameter +- support request hooks + +### Fixed +- bugfixes on CookieSessionMiddleware + +## [1.8.0] - 2016-11-15 +### Added +- leancloud.User#refresh_session_token() +- leancloud.User#is_authenticated() +- keep alive on LeanCloud API Server HTTP connections + +## [1.7.0] - 2016-10-31 +### Fixed +- leancloud.Notification now support fetch() +- allow return a nested structure of leancloud.Object in rpc call +### Added +- define cloud function with alternate function name +- support dev mode in leancloud.push.send +- leancloud.Engine#current request local variable +- session token WSGI middleware + +## [1.6.5] - 2016-09-27 +### Fixed +- LeanEngine on_login / on_verified bugfix + +## [1.6.4] - 2016-09-26 +### Fixed +- LeanEngine CORS middleware error in Python3 +- missing objectId while using Object._deep_save to update an object +- allow Object.get method get objectId / createAt/ updatedAt field +### Updated +- leancloud.User#get_roles() +- rename leancloud.Engine#on_bigquery to leancloud.Engine#on_insight + +## [1.6.3] - 2016-08-03 +### Fixed +- deprecate leancloud.Query.does_not_exists and add leancloud.Query.does_not_exist +### Updated +- updated pyi stub files + +## [1.6.2] - 2016-07-21 +### Fixed +- invalid push_time format in leancloud.push.send function +### Updated +- add leancloud.Object.as_class static function + ## [1.6.1] - 2016-07-08 ### Added - leancloud.User.reset_password_by_sms_code @@ -6,9 +323,10 @@ - refactor file upload process - clean up leancloud.Object fields (reduce memory size) - add type interface for python3.5's type announce +- raise ValueError while pass a None value to leancloud.Query.near ## [1.6.0] - 2016-05-23 -### Updatred +### Updated - support python 3.5 ## [1.5.1] - 2016-05-19 diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index c1be19aa..00000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -arrow -iso8601 -qiniu -requests -werkzeug -wsgi-intercept -nose diff --git a/docs/.buildinfo b/docs/.buildinfo new file mode 100644 index 00000000..e70e3068 --- /dev/null +++ b/docs/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 047595274f1acf162baa7e5230ad15ab +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/docs/_modules/index.html b/docs/_modules/index.html new file mode 100644 index 00000000..7f26ef1c --- /dev/null +++ b/docs/_modules/index.html @@ -0,0 +1,112 @@ + + + + + + 概览:模块代码 — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • 概览:模块代码
  • +
  • +
  • +
+
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/acl.html b/docs/_modules/leancloud/acl.html new file mode 100644 index 00000000..a411746f --- /dev/null +++ b/docs/_modules/leancloud/acl.html @@ -0,0 +1,202 @@ + + + + + + leancloud.acl — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.acl 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import six
+
+import leancloud
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+PUBLIC_KEY = "*"
+
+
+
[文档]class ACL(object): + def __init__(self, permissions_by_id=None): + self.permissions_by_id = permissions_by_id or {} + +
[文档] def dump(self): + return self.permissions_by_id
+ + def _set_access(self, access_type, user_id, allowed): + if isinstance(user_id, leancloud.User): + user_id = user_id.id + elif isinstance(user_id, leancloud.Role): + user_id = "role:" + user_id.get_name() + permissions = self.permissions_by_id.get(user_id) + if permissions is None: + if not allowed: + return + permissions = {} + self.permissions_by_id[user_id] = permissions + + if allowed: + self.permissions_by_id[user_id][access_type] = True + elif access_type in self.permissions_by_id[user_id]: + del self.permissions_by_id[user_id][access_type] + if not self.permissions_by_id[user_id]: + del self.permissions_by_id[user_id] + + def _get_access(self, access_type, user_id): + if isinstance(user_id, leancloud.User): + user_id = user_id.id + elif isinstance(user_id, leancloud.Role): + user_id = "role:" + user_id.get_name() + permissions = self.permissions_by_id.get(user_id) + if not permissions: + return False + return permissions.get(access_type, False) + +
[文档] def set_read_access(self, user_id, allowed): + return self._set_access("read", user_id, allowed)
+ +
[文档] def get_read_access(self, user_id): + return self._get_access("read", user_id)
+ +
[文档] def set_write_access(self, user_id, allowed): + return self._set_access("write", user_id, allowed)
+ +
[文档] def get_write_access(self, user_id): + return self._get_access("write", user_id)
+ +
[文档] def set_public_read_access(self, allowed): + return self.set_read_access(PUBLIC_KEY, allowed)
+ +
[文档] def get_public_read_access(self): + return self.get_read_access(PUBLIC_KEY)
+ +
[文档] def set_public_write_access(self, allowed): + return self.set_write_access(PUBLIC_KEY, allowed)
+ +
[文档] def get_public_write_access(self): + return self.get_write_access(PUBLIC_KEY)
+ +
[文档] def set_role_read_access(self, role, allowed): + if isinstance(role, leancloud.Role): + role = role.get_name() + if not isinstance(role, six.string_types): + raise TypeError("role must be a leancloud.Role or str") + self.set_read_access("role:{0}".format(role), allowed)
+ +
[文档] def get_role_read_access(self, role): + if isinstance(role, leancloud.Role): + role = role.get_name() + if not isinstance(role, six.string_types): + raise TypeError("role must be a leancloud.Role or str") + return self.get_read_access("role:{0}".format(role))
+ +
[文档] def set_role_write_access(self, role, allowed): + if isinstance(role, leancloud.Role): + role = role.get_name() + if not isinstance(role, six.string_types): + raise TypeError("role must be a leancloud.Role or str") + self.set_write_access("role:{0}".format(role), allowed)
+ +
[文档] def get_role_write_access(self, role): + if isinstance(role, leancloud.Role): + role = role.get_name() + if not isinstance(role, six.string_types): + raise TypeError("role must be a leancloud.Role or str") + return self.get_write_access("role:{0}".format(role))
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/client.html b/docs/_modules/leancloud/client.html new file mode 100644 index 00000000..b22a55fa --- /dev/null +++ b/docs/_modules/leancloud/client.html @@ -0,0 +1,360 @@ + + + + + + leancloud.client — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.client 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+import json
+import time
+import hashlib
+import functools
+
+import six
+import requests
+
+import leancloud
+from leancloud import utils
+from leancloud.app_router import AppRouter
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+APP_ID = None
+APP_KEY = None
+MASTER_KEY = None
+HOOK_KEY = None
+if os.getenv("LEANCLOUD_APP_ENV") == "production":
+    USE_PRODUCTION = "1"
+elif os.getenv("LEANCLOUD_APP_ENV") == "stage":
+    USE_PRODUCTION = "0"
+else:  # probably on local machine
+    if os.getenv("LEAN_CLI_HAVE_STAGING") == "true":
+        USE_PRODUCTION = "0"
+    else:  # free trial instance only
+        USE_PRODUCTION = "1"
+
+USE_HTTPS = True
+# 兼容老版本,如果 USE_MASTER_KEY 为 None ,并且 MASTER_KEY 不为 None,则使用 MASTER_KEY
+# 否则依据 USE_MASTER_KEY 来决定是否使用 MASTER_KEY
+USE_MASTER_KEY = None
+REGION = "CN"
+
+app_router = None
+session = requests.Session()
+request_hooks = {}
+
+SERVER_VERSION = "1.1"
+
+TIMEOUT_SECONDS = 15
+
+
+
[文档]def init(app_id, app_key=None, master_key=None, hook_key=None): + """初始化 LeanCloud 的 AppId / AppKey / MasterKey + + :type app_id: string_types + :param app_id: 应用的 Application ID + :type app_key: None or string_types + :param app_key: 应用的 Application Key + :type master_key: None or string_types + :param master_key: 应用的 Master Key + :param hook_key: application's hook key + :type hook_key: None or string_type + """ + if (not app_key) and (not master_key): + raise RuntimeError("app_key or master_key must be specified") + global APP_ID, APP_KEY, MASTER_KEY, HOOK_KEY + APP_ID = app_id + APP_KEY = app_key + MASTER_KEY = master_key + if hook_key: + HOOK_KEY = hook_key + else: + HOOK_KEY = os.environ.get("LEANCLOUD_APP_HOOK_KEY")
+ + +def need_init(func): + @functools.wraps(func) + def new_func(*args, **kwargs): + if APP_ID is None: + raise RuntimeError("LeanCloud SDK must be initialized") + + headers = { + "Content-Type": "application/json;charset=utf-8", + "X-LC-Id": APP_ID, + "X-LC-Hook-Key": HOOK_KEY, + "X-LC-Prod": USE_PRODUCTION, + "User-Agent": "AVOS Cloud python-{0} SDK ({1}.{2})".format( + leancloud.__version__, + leancloud.version_info.major, + leancloud.version_info.minor, + ), + } + md5sum = hashlib.md5() + current_time = six.text_type(int(time.time() * 1000)) + if (USE_MASTER_KEY is None and MASTER_KEY) or USE_MASTER_KEY is True: + md5sum.update((current_time + MASTER_KEY).encode("utf-8")) + headers["X-LC-Sign"] = md5sum.hexdigest() + "," + current_time + ",master" + else: + # In python 2.x, you can feed this object with arbitrary + # strings using the update() method, but in python 3.x, + # you should feed with bytes-like objects. + md5sum.update((current_time + APP_KEY).encode("utf-8")) + headers["X-LC-Sign"] = md5sum.hexdigest() + "," + current_time + + user = leancloud.User.get_current() + if user: + headers["X-LC-Session"] = user._session_token + + return func(headers=headers, *args, **kwargs) + + return new_func + + +def get_url(part): + # try to use the base URL from environ + url = os.environ.get("LC_API_SERVER") or os.environ.get("LEANCLOUD_API_SERVER") + if url: + return "{}/{}{}".format(url, SERVER_VERSION, part) + + global app_router + if app_router is None: + app_router = AppRouter(APP_ID, REGION) + + if part.startswith("/push") or part.startswith("/installations"): + host = app_router.get("push") + elif part.startswith("/collect"): + host = app_router.get("stats") + elif part.startswith("/functions") or part.startswith("/call"): + host = app_router.get("engine") + else: + host = app_router.get("api") + r = { + "schema": "https" if USE_HTTPS else "http", + "version": SERVER_VERSION, + "host": host, + "part": part, + } + return "{schema}://{host}/{version}{part}".format(**r) + + +
[文档]def use_production(flag): + """调用生产环境 / 开发环境的 cloud func / cloud hook + 默认调用生产环境。 + """ + global USE_PRODUCTION + USE_PRODUCTION = "1" if flag else "0"
+ + +
[文档]def use_master_key(flag=True): + """是否使用 master key 发送请求。 + 如果不调用此函数,会根据 leancloud.init 的参数来决定是否使用 master key。 + + :type flag: bool + """ + global USE_MASTER_KEY + if not flag: + USE_MASTER_KEY = False + return + if not MASTER_KEY: + raise RuntimeError("LeanCloud SDK master key not specified") + USE_MASTER_KEY = True
+ + +def check_error(func): + @functools.wraps(func) + def new_func(*args, **kwargs): + response = func(*args, **kwargs) + assert isinstance(response, requests.Response) + if response.headers.get("Content-Type") == "text/html": + raise leancloud.LeanCloudError(-1, "Bad Request") + + content = response.json() + + if "error" in content: + raise leancloud.LeanCloudError( + content.get("code", 1), content.get("error", "Unknown Error") + ) + + return response + + return new_func + + +
[文档]def use_region(region): + if region not in ("CN", "US"): + raise ValueError("currently no nodes in the region") + + global REGION + REGION = region
+ + +def get_server_time(): + response = check_error(session.get)(get_url("/date"), timeout=TIMEOUT_SECONDS) + return utils.decode("iso", response.json()) + + +def get_app_info(): + return { + "app_id": APP_ID, + "app_key": APP_KEY, + "master_key": MASTER_KEY, + "hook_key": HOOK_KEY, + } + + +@need_init +@check_error +def get(url, params=None, headers=None): + if not params: + params = {} + else: + for k, v in six.iteritems(params): + if isinstance(v, dict): + params[k] = json.dumps(v, separators=(",", ":")) + response = session.get( + get_url(url), + headers=headers, + params=params, + timeout=TIMEOUT_SECONDS, + hooks=request_hooks, + ) + return response + + +@need_init +@check_error +def post(url, params, headers=None): + response = session.post( + get_url(url), + headers=headers, + data=json.dumps(params, separators=(",", ":")), + timeout=TIMEOUT_SECONDS, + hooks=request_hooks, + ) + return response + + +@need_init +@check_error +def put(url, params, headers=None): + response = session.put( + get_url(url), + headers=headers, + data=json.dumps(params, separators=(",", ":")), + timeout=TIMEOUT_SECONDS, + hooks=request_hooks, + ) + return response + + +@need_init +@check_error +def delete(url, params=None, headers=None): + response = session.delete( + get_url(url), + headers=headers, + data=json.dumps(params, separators=(",", ":")), + timeout=TIMEOUT_SECONDS, + hooks=request_hooks, + ) + return response +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/cloud.html b/docs/_modules/leancloud/cloud.html new file mode 100644 index 00000000..b581e2ef --- /dev/null +++ b/docs/_modules/leancloud/cloud.html @@ -0,0 +1,287 @@ + + + + + + leancloud.cloud — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.cloud 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import six
+
+import leancloud
+from leancloud import utils
+from leancloud.engine import leanengine
+
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+
[文档]def run(_cloud_func_name, **params): + """ + 调用 LeanEngine 上的远程代码 + :param name: 需要调用的远程 Cloud Code 的名称 + :type name: string_types + :param params: 调用参数 + :return: 调用结果 + """ + response = leancloud.client.post( + "/functions/{0}".format(_cloud_func_name), params=params + ) + content = response.json() + return utils.decode(None, content)["result"]
+ + +def _run_in_local(_cloud_func_name, **params): + if not leanengine.root_engine: + return + result = leanengine.dispatch_cloud_func( + leanengine.root_engine.app.cloud_codes, {}, _cloud_func_name, False, params + ) + return utils.decode(None, result) + + +run.remote = run +run.local = _run_in_local + + +
[文档]def rpc(_cloud_rpc_name, **params): + """ + 调用 LeanEngine 上的远程代码 + 与 cloud.run 类似,但是允许传入 leancloud.Object 作为参数,也允许传入 leancloud.Object 作为结果 + :param name: 需要调用的远程 Cloud Code 的名称 + :type name: basestring + :param params: 调用参数 + :return: 调用结果 + """ + encoded_params = {} + for key, value in params.items(): + if isinstance(params, leancloud.Object): + encoded_params[key] = utils.encode(value._dump()) + else: + encoded_params[key] = utils.encode(value) + response = leancloud.client.post( + "/call/{}".format(_cloud_rpc_name), params=encoded_params + ) + content = response.json() + return utils.decode(None, content["result"])
+ + +def _rpc_in_local(_cloud_rpc_name, **params): + if not leanengine.root_engine: + return + result = leanengine.dispatch_cloud_func( + leanengine.root_engine.app.cloud_codes, {}, _cloud_rpc_name, True, params + ) + return utils.decode(None, result) + + +rpc.remote = rpc +rpc.local = _rpc_in_local + + +
[文档]def request_sms_code( + phone_number, + idd="+86", + sms_type="sms", + validate_token=None, + template=None, + sign=None, + params=None, +): + """ + 请求发送手机验证码 + :param phone_number: 需要验证的手机号码 + :param idd: 号码的所在地国家代码,默认为中国(+86) + :param sms_type: 验证码发送方式,'voice' 为语音,'sms' 为短信 + :param template: 模版名称 + :param sign: 短信签名名称 + :return: None + """ + if not isinstance(phone_number, six.string_types): + raise TypeError("phone_number must be a string") + + data = { + "mobilePhoneNumber": phone_number + if phone_number.startswith("+") + else idd + phone_number, + "smsType": sms_type, + } + + if template is not None: + data["template"] = template + + if sign is not None: + data["sign"] = sign + + if validate_token is not None: + data["validate_token"] = validate_token + + if params is not None: + data.update(params) + + leancloud.client.post("/requestSmsCode", params=data)
+ + +
[文档]def verify_sms_code(phone_number, code): + """ + 获取到手机验证码之后,验证验证码是否正确。如果验证失败,抛出异常。 + :param phone_number: 需要验证的手机号码 + :param code: 接受到的验证码 + :return: None + """ + params = { + "mobilePhoneNumber": phone_number, + } + leancloud.client.post("/verifySmsCode/{0}".format(code), params=params) + return True
+ + +
[文档]class Captcha(object): + """ + 表示图形验证码 + """ + + def __init__(self, token, url): + self.token = token + self.url = url + +
[文档] def verify(self, code): + """ + 验证用户输入与图形验证码是否匹配 + :params code: 用户填写的验证码 + """ + return verify_captcha(code, self.token)
+ + +
[文档]def request_captcha(size=None, width=None, height=None, ttl=None): + """ + 请求生成新的图形验证码 + :return: Captcha + """ + params = { + "size": size, + "width": width, + "height": height, + "ttl": ttl, + } + params = {k: v for k, v in params.items() if v is not None} + + response = leancloud.client.get("/requestCaptcha", params) + content = response.json() + return Captcha(content["captcha_token"], content["captcha_url"])
+ + +
[文档]def verify_captcha(code, token): + """ + 验证用户输入与图形验证码是否匹配 + :params code: 用户填写的验证码 + :params token: 图形验证码对应的 token + :return: validate token + """ + params = { + "captcha_token": token, + "captcha_code": code, + } + response = leancloud.client.post("/verifyCaptcha", params) + return response.json()["validate_token"]
+ + +
[文档]def get_server_time(): + return leancloud.client.get_server_time()
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/engine.html b/docs/_modules/leancloud/engine.html new file mode 100644 index 00000000..8ab5dd6d --- /dev/null +++ b/docs/_modules/leancloud/engine.html @@ -0,0 +1,290 @@ + + + + + + leancloud.engine — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.engine 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+import sys
+import json
+import warnings
+
+from werkzeug.wrappers import Request
+from werkzeug.wrappers import Response
+from werkzeug.serving import run_simple
+
+import leancloud
+from . import utils
+from . import leanengine
+from .authorization import AuthorizationMiddleware
+from .cookie_session import CookieSessionMiddleware  # noqa: F401
+from .cors import CORSMiddleware
+from .https_redirect_middleware import HttpsRedirectMiddleware  # noqa: F401
+from .leanengine import LeanEngineApplication
+from .leanengine import LeanEngineError
+from .leanengine import after_delete
+from .leanengine import after_save
+from .leanengine import after_update
+from .leanengine import before_delete
+from .leanengine import before_save
+from .leanengine import before_update
+from .leanengine import context
+from .leanengine import current
+from .leanengine import register_cloud_func
+from .leanengine import register_on_bigquery
+from .leanengine import register_on_login
+from .leanengine import register_on_auth_data
+from .leanengine import register_on_verified
+from .leanengine import user
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+
[文档]class Engine(object): + """ + LeanEngine middleware. + """ + + def __init__(self, wsgi_app=None, fetch_user=True): + """ + LeanEngine middleware constructor. + + :param wsgi_app: wsgi callable + :param fetch_user: + should fetch user's data from server while prNoneocessing session token. + :type fetch_user: bool + """ + self.current = current + if wsgi_app: + leanengine.root_engine = self + self.origin_app = wsgi_app + self.app = LeanEngineApplication(fetch_user=fetch_user) + self.cloud_app = context.local_manager.make_middleware( + CORSMiddleware(AuthorizationMiddleware(self.app)) + ) + + def __call__(self, environ, start_response): + request = Request(environ) + environ[ + "leanengine.request" + ] = request # cache werkzeug request for other middlewares + + if request.path in ("/__engine/1/ping", "/__engine/1.1/ping/"): + start_response( + utils.to_native("200 OK"), + [ + ( + utils.to_native("Content-Type"), + utils.to_native("application/json"), + ) + ], + ) + version = sys.version_info + return Response( + json.dumps( + { + "version": leancloud.__version__, + "runtime": "cpython-{0}.{1}.{2}".format( + version.major, version.minor, version.micro + ), + } + ) + )(environ, start_response) + if request.path.startswith("/__engine/"): + return self.cloud_app(environ, start_response) + if request.path.startswith("/1/functions") or request.path.startswith( + "/1.1/functions" + ): + return self.cloud_app(environ, start_response) + if request.path.startswith("/1/call") or request.path.startswith("/1.1/call"): + return self.cloud_app(environ, start_response) + return self.origin_app(environ, start_response) + +
[文档] def wrap(self, wsgi_app): + if leanengine.root_engine: + raise RuntimeError("It's forbidden that overwriting wsgi_func.") + leanengine.root_engine = self + self.origin_app = wsgi_app + return self
+ +
[文档] def register(self, engine): + if not isinstance(engine, Engine): + raise TypeError("Please specify an Engine instance") + self.app.update_cloud_codes(engine.app.cloud_codes)
+ +
[文档] def define(self, *args, **kwargs): + return register_cloud_func(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def on_verified(self, *args, **kwargs): + return register_on_verified(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def on_login(self, *args, **kwargs): + return register_on_login(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def on_auth_data(self, *args, **kwargs): + return register_on_auth_data(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def on_bigquery(self, *args, **kwargs): + warnings.warn( + "on_bigquery is deprecated, please use on_insight instead", + leancloud.LeanCloudWarning, + ) + return register_on_bigquery(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def before_save(self, *args, **kwargs): + return before_save(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def after_save(self, *args, **kwargs): + return after_save(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def before_update(self, *args, **kwargs): + return before_update(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def after_update(self, *args, **kwargs): + return after_update(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def before_delete(self, *args, **kwargs): + return before_delete(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def after_delete(self, *args, **kwargs): + return after_delete(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def on_insight(self, *args, **kwargs): + return register_on_bigquery(self.app.cloud_codes, *args, **kwargs)
+ +
[文档] def run(self, *args, **kwargs): + return run_simple(*args, **kwargs)
+ +
[文档] def start(self): + from gevent.pywsgi import WSGIServer + + if not hasattr(leancloud, "APP_ID"): + APP_ID = os.environ["LEANCLOUD_APP_ID"] + APP_KEY = os.environ["LEANCLOUD_APP_KEY"] + MASTER_KEY = os.environ["LEANCLOUD_APP_MASTER_KEY"] + HOOK_KEY = os.environ["LEANCLOUD_APP_HOOK_KEY"] + PORT = int(os.environ.get("LEANCLOUD_APP_PORT")) + leancloud.init( + APP_ID, app_key=APP_KEY, master_key=MASTER_KEY, hook_key=HOOK_KEY + ) + + def application(environ, start_response): + start_response( + "200 OK".encode("utf-8"), + [("Content-Type".encode("utf-8"), "text/plain".encode("utf-8"))], + ) + return "This is a LeanEngine application." + + class NopLogger(object): + def write(self, s): + pass + + app = self.wrap(application) + self.server = WSGIServer(("", PORT), app, log=NopLogger()) + print("LeanEngine Cloud Functions app is running, port:", PORT) + self.server.serve_forever()
+ +
[文档] def stop(self): + self.server.stop()
+ + +__all__ = ["user", "Engine", "LeanEngineError"] +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/engine/cookie_session.html b/docs/_modules/leancloud/engine/cookie_session.html new file mode 100644 index 00000000..36c895d3 --- /dev/null +++ b/docs/_modules/leancloud/engine/cookie_session.html @@ -0,0 +1,220 @@ + + + + + + leancloud.engine.cookie_session — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.engine.cookie_session 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from werkzeug import http
+from werkzeug.wrappers import Request
+from secure_cookie.cookie import SecureCookie
+
+from . import utils
+from leancloud.user import User
+
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+
[文档]class CookieSessionMiddleware(object): + """ + 用来在 webhosting 功能中实现自动管理 LeanCloud 用户登录状态的 WSGI 中间件。 + 使用此中间件之后,在处理 web 请求中调用了 `leancloud.User.login()` 方法登录成功后, + 会将此用户 session token 写入到 cookie 中。 + 后续此次会话都可以通过 `leancloud.User.get_current()` 获取到此用户对象。 + + :param secret: 对保存在 cookie 中的用户 session token 进行签名时需要的 key,可使用任意方法随机生成,请不要泄漏 + :type secret: str + :param name: 在 cookie 中保存的 session token 的 key 的名称,默认为 "leancloud:session" + :type name: str + :param excluded_paths: + 指定哪些 URL path 不处理 session token,比如在处理静态文件的 URL path 上不进行处理,防止无谓的性能浪费 + :type excluded_paths: list + :param fetch_user: 处理请求时是否要从存储服务获取用户数据, + 如果为 false 的话, + leancloud.User.get_current() 获取到的用户数据上除了 session_token 之外没有任何其他数据, + 需要自己调用 fetch() 来获取。 + 为 true 的话,会自动在用户对象上调用 fetch(),这样将会产生一次数据存储的 API 调用。 + 默认为 false + :type fetch_user: bool + :param expires: 设置 cookie 的 expires + :type expires: int or datetime + :param max_age: 设置 cookie 的 max_age,单位为秒 + :type max_age: int + """ + + def __init__( + self, + app, + secret, + name="leancloud:session", + excluded_paths=None, + fetch_user=False, + expires=None, + max_age=None, + ): + if not secret: + raise RuntimeError("secret is required") + self.fetch_user = fetch_user + self.secret = secret + self.app = app + self.name = name + self.excluded_paths = [ + "/__engine/", + "/1/functions/", + "/1.1/functions/", + "/1/call/", + "/1.1/call/", + ] + self.expires = expires + self.max_age = max_age + if excluded_paths: + self.excluded_paths += excluded_paths + + def __call__(self, environ, start_response): + self.pre_process(environ) + + def new_start_response(status, response_headers, exc_info=None): + self.post_process(environ, response_headers) + return start_response(status, response_headers, exc_info) + + return self.app(environ, new_start_response) + +
[文档] def pre_process(self, environ): + request = Request(environ) + for prefix in self.excluded_paths: + if request.path.startswith(prefix): + return + + cookie = request.cookies.get(self.name) + if not cookie: + return + + session = SecureCookie.unserialize(cookie, self.secret) + + if "session_token" not in session: + return + + if not self.fetch_user: + user = User() + user._session_token = session["session_token"] + user.id = session["uid"] + User.set_current(user) + else: + user = User.become(session["session_token"]) + User.set_current(user)
+ +
[文档] def post_process(self, environ, headers): + user = User.get_current() + if not user: + cookies = http.parse_cookie(environ) + if self.name in cookies: + raw = http.dump_cookie(self.name, "", expires=1) + headers.append((utils.to_native("Set-Cookie"), raw)) + return + cookie = SecureCookie( + {"uid": user.id, "session_token": user.get_session_token()}, self.secret + ) + raw = http.dump_cookie( + self.name, cookie.serialize(), expires=self.expires, max_age=self.max_age + ) + headers.append((utils.to_native("Set-Cookie"), raw))
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/engine/https_redirect_middleware.html b/docs/_modules/leancloud/engine/https_redirect_middleware.html new file mode 100644 index 00000000..1ea771e7 --- /dev/null +++ b/docs/_modules/leancloud/engine/https_redirect_middleware.html @@ -0,0 +1,134 @@ + + + + + + leancloud.engine.https_redirect_middleware — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.engine.https_redirect_middleware 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+
+
+from werkzeug.wrappers import Request
+from werkzeug.utils import redirect
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+is_prod = True if os.environ.get("LEANCLOUD_APP_ENV") == "production" else False
+
+
+
[文档]class HttpsRedirectMiddleware(object): + def __init__(self, wsgi_app): + self.origin_app = wsgi_app + + def __call__(self, environ, start_response): + request = Request(environ) + engine_health = "/1.1/functions/_ops/metadatas" + if ( + is_prod + and request.path != engine_health + and request.headers.get("X-Forwarded-Proto") != "https" + ): + url = "https://{0}{1}".format(request.host, request.full_path) + return redirect(url)(environ, start_response) + + return self.origin_app(environ, start_response)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/errors.html b/docs/_modules/leancloud/errors.html new file mode 100644 index 00000000..d01593ee --- /dev/null +++ b/docs/_modules/leancloud/errors.html @@ -0,0 +1,127 @@ + + + + + + leancloud.errors — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.errors 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import six
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+
[文档]@six.python_2_unicode_compatible +class LeanCloudError(Exception): + def __init__(self, code, error): + self.code = code + self.error = error + + def __str__(self): + error = ( + self.error + if isinstance(self.error, six.text_type) + else self.error.encode("utf-8", "ignore") + ) + return "LeanCloudError: [{0}] {1}".format(self.code, error)
+ + +
[文档]class LeanCloudWarning(UserWarning): + pass
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/file_.html b/docs/_modules/leancloud/file_.html new file mode 100644 index 00000000..b79d9209 --- /dev/null +++ b/docs/_modules/leancloud/file_.html @@ -0,0 +1,434 @@ + + + + + + leancloud.file_ — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.file_ 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+import re
+import io
+import hashlib
+import logging
+import threading
+
+import six
+import requests
+
+import leancloud
+from leancloud import client
+from leancloud import utils
+from leancloud.errors import LeanCloudError
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+logger = logging.getLogger(__name__)
+
+
+DEFAULT_TIMEOUT = 30
+
+
+
[文档]class File(object): + _class_name = "_File" # walks like a leancloud.Object + + def __init__(self, name="", data=None, mime_type=None): + self._name = name + self.key = None + self.id = None + self.created_at = None + self.updated_at = None + self._url = None + self._successful_url = None + self._acl = None + self.current_user = leancloud.User.get_current() + self.timeout = 30 + self._metadata = {"owner": "unknown"} + if ( + self.current_user and self.current_user != None + ): # NOQA: self.current_user may be a thread_local object + self._metadata["owner"] = self.current_user.id + + pattern = re.compile(r"\.([^.]*)$") + extension = pattern.findall(name) + if extension: + self.extension = extension[0].lower() + else: + self.extension = "" + + self._mime_type = mime_type + + if data is None: + self._source = None + return + + try: + data.read + data.tell + data.seek(0, os.SEEK_END) + data.seek(0, os.SEEK_SET) + except Exception: + if (six.PY3 and isinstance(data, (memoryview, bytes))) or ( + six.PY2 and isinstance(data, (buffer, memoryview, str)) # noqa: F821 + ): + data = io.BytesIO(data) + elif data.read: + data = io.BytesIO(data.read()) + else: + raise TypeError( + "Do not know how to handle data, accepts file like object or bytes" + ) + + data.seek(0, os.SEEK_SET) + checksum = hashlib.md5() + while True: + chunk = data.read(4096) + if not chunk: + break + + try: + checksum.update(chunk) + except TypeError: + checksum.update(chunk.encode("utf-8")) + + self._metadata["_checksum"] = checksum.hexdigest() + self._metadata["size"] = data.tell() + + # 3.5MB, 1Mbps * 30s + # increase timeout + if self._metadata["size"] > 3750000: + self.timeout = self.timeout * int(self._metadata["size"] / 3750000) + + data.seek(0, os.SEEK_SET) + + self._source = data + + @utils.classproperty + def query(self): + return leancloud.Query(self) + +
[文档] @classmethod + def create_with_url(cls, name, url, meta_data=None, mime_type=None): + f = File(name, None, mime_type) + if meta_data: + f._metadata.update(meta_data) + + if isinstance(url, six.string_types): + f._url = url + else: + raise ValueError("url must be a str / unicode") + + f._metadata["__source"] = "external" + return f
+ +
[文档] @classmethod + def create_without_data(cls, object_id): + f = File("") + f.id = object_id + return f
+ +
[文档] def get_acl(self): + return self._acl
+ +
[文档] def set_acl(self, acl): + if not isinstance(acl, leancloud.ACL): + raise TypeError("acl must be a leancloud.ACL instance") + self._acl = acl
+ + @property + def name(self): + return self._name + + @property + def url(self): + return self._successful_url + + @property + def mime_type(self): + return self._mime_type + + @mime_type.setter + def set_mime_type(self, mime_type): + self._mime_type = mime_type + + @property + def size(self): + return self._metadata["size"] + + @property + def owner_id(self): + return self._metadata["owner"] + + @property + def metadata(self): + return self._metadata + +
[文档] def get_thumbnail_url( + self, width, height, quality=100, scale_to_fit=True, fmt="png" + ): + if not self.url: + raise ValueError("invalid url") + + if width < 0 or height < 0: + raise ValueError("invalid height or width params") + + if quality > 100 or quality <= 0: + raise ValueError("quality must between 0 and 100") + + mode = 2 if scale_to_fit else 1 + + return self.url + "?imageView/{0}/w/{1}/h/{2}/q/{3}/format/{4}".format( + mode, width, height, quality, fmt + )
+ +
[文档] def destroy(self): + if not self.id: + return False + response = client.delete("/files/{0}".format(self.id)) + if response.status_code != 200: + raise LeanCloudError(1, "the file is not sucessfully destroyed")
+ + + def _save_to_qiniu(self, token, key): + self._source.seek(0) + + import qiniu + + qiniu.set_default(connection_timeout=self.timeout) + ret, info = qiniu.put_data(token, key, self._source) + self._source.seek(0) + + if info.status_code != 200: + self._save_callback(token, False) + raise LeanCloudError( + 1, + "the file is not saved, qiniu status code: {0}".format( + info.status_code + ), + ) + self._save_callback(token, True) + + def _save_to_s3(self, token, upload_url): + self._source.seek(0) + response = requests.put( + upload_url, data=self._source, headers={"Content-Type": self.mime_type} + ) + if response.status_code != 200: + self._save_callback(token, False) + raise LeanCloudError(1, "The file is not successfully saved to S3") + self._source.seek(0) + self._save_callback(token, True) + + def _save_external(self): + data = { + "name": self._name, + "ACL": self._acl, + "metaData": self._metadata, + "mime_type": self.mime_type, + "url": self._url, + } + response = client.post("/files".format(self._name), data) + content = response.json() + + self.id = content["objectId"] + + self._successful_url = self._url + + _created_at = utils.decode_date_string(content.get("createdAt")) + _updated_at = utils.decode_updated_at(content.get("updatedAt"), _created_at) + if _created_at is not None: + self.created_at = _created_at + if _updated_at is not None: + self.updated_at = _updated_at + + def _save_to_qcloud(self, token, upload_url): + headers = { + "Authorization": token, + } + self._source.seek(0) + data = { + "op": "upload", + "filecontent": self._source.read(), + } + response = requests.post(upload_url, headers=headers, files=data) + self._source.seek(0) + info = response.json() + if info["code"] != 0: + self._save_callback(token, False) + raise LeanCloudError( + 1, + "this file is not saved, qcloud cos status code: {}".format( + info["code"] + ), + ) + self._save_callback(token, True) + + def _save_callback(self, token, successed): + if not token: + return + + def f(): + try: + client.post("/fileCallback", {"token": token, "result": successed}) + except LeanCloudError as e: + logger.warning("call file callback failed, error: %s", e) + + threading.Thread(target=f).start() + +
[文档] def save(self): + if self._url and self.metadata.get("__source") == "external": + self._save_external() + elif not self._source: + pass + else: + content = self._get_file_token() + self._mime_type = content["mime_type"] + if content["provider"] == "qiniu": + self._save_to_qiniu(content["token"], content["key"]) + elif content["provider"] == "qcloud": + self._save_to_qcloud(content["token"], content["upload_url"]) + elif content["provider"] == "s3": + self._save_to_s3(content.get("token"), content["upload_url"]) + else: + raise RuntimeError("The provider field in the fetched content is empty") + self._update_data(content)
+ + def _update_data(self, server_data): + if "objectId" in server_data: + self.id = server_data.get("objectId") + if "name" in server_data: + self._name = server_data.get("name") + if "url" in server_data: + self._url = server_data.get("url") + self._successful_url = self._url + if "key" in server_data: + self.key = server_data.get("key") + if "mime_type" in server_data: + self._mime_type = server_data["mime_type"] + if "metaData" in server_data: + self._metadata = server_data.get("metaData") + + _created_at = utils.decode_date_string(server_data.get("createdAt")) + _updated_at = utils.decode_updated_at(server_data.get("updatedAt"), _created_at) + if _created_at is not None: + self.created_at = _created_at + if _updated_at is not None: + self.updated_at = _updated_at + + def _get_file_token(self): + data = { + "name": self._name, + "ACL": self._acl, + "mime_type": self.mime_type, + "metaData": self._metadata, + } + if self.key is not None: + data["key"] = self.key + response = client.post("/fileTokens", data) + content = response.json() + self.id = content["objectId"] + self._url = content["url"] + self.key = content["key"] + return content + +
[文档] def fetch(self): + response = client.get("/files/{0}".format(self.id)) + content = response.json() + self._update_data(content)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/geo_point.html b/docs/_modules/leancloud/geo_point.html new file mode 100644 index 00000000..6b94f031 --- /dev/null +++ b/docs/_modules/leancloud/geo_point.html @@ -0,0 +1,227 @@ + + + + + + leancloud.geo_point — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.geo_point 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import math
+
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+
[文档]class GeoPoint(object): + def __init__(self, latitude=0, longitude=0): + """ + + :param latitude: 纬度 + :type latitude: int or float + :param longitude: 经度 + :type longitude: int or float + :return: GeoPoint + """ + self._validate(latitude, longitude) + self._latitude = latitude + self._longitude = longitude + + @classmethod + def _validate(cls, latitude, longitude): + if latitude < -90.0: + raise ValueError("GeoPoint latitude {0} < -90.0".format(latitude)) + + if latitude > 90.0: + raise ValueError("GeoPoint latitude {0} > 90.0".format(latitude)) + + if longitude < -180.0: + raise ValueError("GeoPoint longitude {0} < -180.0".format(longitude)) + + if longitude > 180.0: + raise ValueError("GeoPoint longitude {0} > 180.0".format(longitude)) + + @property + def latitude(self): + """ + 当前对象的纬度 + """ + return self._latitude + + @latitude.setter + def latitude(self, latitude): + self._validate(latitude, self.longitude) + self._latitude = latitude + + @property + def longitude(self): + """ + 当前对象的经度 + """ + return self._longitude + + @longitude.setter + def longitude(self, longitude): + self._validate(self.latitude, longitude) + self._longitude = longitude + +
[文档] def dump(self): + self._validate(self.latitude, self.longitude) + return { + "__type": "GeoPoint", + "latitude": self.latitude, + "longitude": self.longitude, + }
+ +
[文档] def radians_to(self, other): + """ + Returns the distance from this GeoPoint to another in radians. + + :param other: point the other GeoPoint + :type other: GeoPoint + :rtype: float + """ + d2r = math.pi / 180.0 + lat1rad = self.latitude * d2r + long1rad = self.longitude * d2r + + lat2rad = other.latitude * d2r + long2rad = other.longitude * d2r + + delta_lat = lat1rad - lat2rad + delta_long = long1rad - long2rad + + sin_delta_lat_div2 = math.sin(delta_lat / 2.0) + sin_delta_long_div2 = math.sin(delta_long / 2.0) + + a = (sin_delta_lat_div2 * sin_delta_lat_div2) + ( + math.cos(lat1rad) + * math.cos(lat2rad) + * sin_delta_long_div2 + * sin_delta_long_div2 + ) + a = min(1.0, a) + return 2 * math.asin(math.sqrt(a))
+ +
[文档] def kilometers_to(self, other): + """ + Returns the distance from this GeoPoint to another in kilometers. + + :param other: point the other GeoPoint + :type other: GeoPoint + :rtype: float + """ + return self.radians_to(other) * 6371.0
+ +
[文档] def miles_to(self, other): + """ + Returns the distance from this GeoPoint to another in miles. + + :param other: point the other GeoPoint + :type other: GeoPoint + :rtype: float + """ + return self.radians_to(other) * 3958.8
+ + def __eq__(self, other): + return ( + isinstance(other, GeoPoint) + and self.latitude == other.latitude + and self.longitude == other.longitude + )
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/object_.html b/docs/_modules/leancloud/object_.html new file mode 100644 index 00000000..e828e6c4 --- /dev/null +++ b/docs/_modules/leancloud/object_.html @@ -0,0 +1,733 @@ + + + + + + leancloud.object_ — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.object_ 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import copy
+import json
+
+import six
+from werkzeug.local import LocalProxy
+
+import leancloud
+from leancloud import utils
+from leancloud import client
+from leancloud import operation
+
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+object_class_map = {}
+
+
+class ObjectMeta(type):
+    def __new__(mcs, name, bases, attrs):
+        cached_class = object_class_map.get(name)
+        if cached_class:
+            return cached_class
+
+        super_new = super(ObjectMeta, mcs).__new__
+
+        # let user define their class_name at subclass-creation stage
+        class_name = attrs.pop("class_name", None)
+
+        if class_name:
+            attrs["_class_name"] = class_name
+        elif name == "User":
+            attrs["_class_name"] = "_User"
+        elif name == "Installation":
+            attrs["_class_name"] = "_Installation"
+        elif name == "Notification":
+            attrs["_class_name"] = "_Notification"
+        elif name == "Role":
+            attrs["_class_name"] = "_Role"
+        elif name == "Conversation":
+            attrs["_class_name"] = "_Conversation"
+        elif name == "SysMessage":
+            attrs["_class_name"] = "_SysMessage"
+        else:
+            attrs["_class_name"] = name
+
+        object_class = super_new(mcs, name, bases, attrs)
+        object_class_map[name] = object_class
+        return object_class
+
+    @property
+    def query(cls):
+        """
+        获取当前对象的 Query 对象。
+
+        :rtype: leancloud.Query
+        """
+        return leancloud.Query(cls)
+
+
+
[文档]class Object(six.with_metaclass(ObjectMeta, object)): + def __init__(self, **attrs): + """ + 创建一个新的 leancloud.Object + + :param attrs: 对象属性 + :return: + """ + self.id = None + self._class_name = self._class_name # for IDE + self._changes = {} + self._attributes = {} + self._flags = {} + self.created_at = None + self.updated_at = None + + for k, v in six.iteritems(attrs): + self.set(k, v) + +
[文档] @classmethod + def extend(cls, name): + """ + 派生一个新的 leancloud.Object 子类 + + :param name: 子类名称 + :type name: string_types + :return: 派生的子类 + :rtype: ObjectMeta + """ + if six.PY2 and isinstance(name, six.text_type): + # In python2, class name must be a python2 str. + name = name.encode("utf-8") + return type(name, (cls,), {})
+ +
[文档] @classmethod + def create(cls, class_name, **attributes): + """ + 根据参数创建一个 leancloud.Object 的子类的实例化对象 + + :param class_name: 子类名称 + :type class_name: string_types + :param attributes: 对象属性 + :return: 派生子类的实例 + :rtype: Object + """ + object_class = cls.extend(class_name) + return object_class(**attributes)
+ +
[文档] @classmethod + def create_without_data(cls, id_): + """ + 根据 objectId 创建一个 leancloud.Object,代表一个服务器上已经存在的对象。可以调用 fetch 方法来获取服务器上的数据 + + :param id_: 对象的 objectId + :type id_: string_types + :return: 没有数据的对象 + :rtype: Object + """ + if cls is Object: + raise RuntimeError("can not call create_without_data on leancloud.Object") + obj = cls() + obj.id = id_ + return obj
+ +
[文档] @classmethod + def save_all(cls, objs): + """ + 在一个请求中 save 多个 leancloud.Object 对象实例。 + + :param objs: 需要 save 的对象 + :type objs: list + """ + if not objs: + return + return cls()._deep_save(objs, [])
+ +
[文档] @classmethod + def destroy_all(cls, objs): + """ + 在一个请求中 destroy 多个 leancloud.Object 对象实例。 + + :param objs: 需要 destroy 的对象 + :type objs: list + """ + if not objs: + return + if any(x.is_new() for x in objs): + raise ValueError("Could not destroy unsaved object") + + dumped_objs = [] + for obj in objs: + dumped_obj = { + "method": "DELETE", + "path": "/{0}/classes/{1}/{2}".format( + client.SERVER_VERSION, obj._class_name, obj.id + ), + "body": obj._flags, + } + dumped_objs.append(dumped_obj) + + response = client.post("/batch", params={"requests": dumped_objs}).json() + + errors = [] + for idx in range(len(objs)): + content = response[idx] + error = content.get("error") + if error: + errors.append( + leancloud.LeanCloudError(error.get("code"), error.get("error")) + ) + + if errors: + # TODO: how to raise list of errors? + # raise MultipleValidationErrors(errors) + # add test + raise errors[0]
+ +
[文档] def dump(self): + obj = self._dump() + obj.pop("__type") + obj.pop("className") + return obj
+ + def _dump(self): + obj = copy.deepcopy(self._attributes) + for k, v in six.iteritems(obj): + obj[k] = utils.encode(v) + + if self.id is not None: + obj["objectId"] = self.id + + obj["__type"] = "Object" + obj["className"] = self._class_name + return obj + +
[文档] def destroy(self): + """ + 从服务器上删除这个对象 + + :rtype: None + """ + if not self.id: + return + client.delete("/classes/{0}/{1}".format(self._class_name, self.id), self._flags)
+ +
[文档] def save(self, where=None, fetch_when_save=None): + """ + 将对象数据保存至服务器 + + :return: None + :rtype: None + """ + if where and not isinstance(where, leancloud.Query): + raise TypeError( + "where param type should be leancloud.Query, got %s", type(where) + ) + + if where and where._query_class._class_name != self._class_name: + raise TypeError( + "where param's class name not equal to the current object's class name" + ) + + if where and self.is_new(): + raise TypeError("where params works only when leancloud.Object is saved") + + unsaved_children = [] + unsaved_files = [] + self._find_unsaved_children(self._attributes, unsaved_children, unsaved_files) + if unsaved_children or unsaved_files: + self._deep_save(unsaved_children, unsaved_files, exclude=self._attributes) + + data = self._dump_save() + fetch_when_save = "true" if fetch_when_save else "false" + + if self.is_new(): + response = client.post( + "/classes/{0}?fetchWhenSave={1}".format( + self._class_name, fetch_when_save + ), + data, + ) + else: + url = "/classes/{0}/{1}?fetchWhenSave={2}".format( + self._class_name, self.id, fetch_when_save + ) + if where: + url += "&where=" + json.dumps( + where.dump()["where"], separators=(",", ":") + ) + response = client.put(url, data) + + self._update_data(response.json())
+ + def _deep_save(self, unsaved_children, unsaved_files, exclude=None): + if exclude: + unsaved_children = [x for x in unsaved_children if x != exclude] + + for f in unsaved_files: + f.save() + + if not unsaved_children: + return + dumped_objs = [] + for obj in unsaved_children: + if obj.id is None: + method = "POST" + path = "/{0}/classes/{1}".format(client.SERVER_VERSION, obj._class_name) + else: + method = "PUT" + path = "/{0}/classes/{1}/{2}".format( + client.SERVER_VERSION, obj._class_name, obj.id + ) + body = obj._dump_save() + dumped_obj = { + "method": method, + "path": path, + "body": body, + } + dumped_objs.append(dumped_obj) + + response = client.post("/batch", params={"requests": dumped_objs}).json() + + errors = [] + for idx, obj in enumerate(unsaved_children): + content = response[idx] + error = content.get("error") + if error: + errors.append( + leancloud.LeanCloudError(error.get("code"), error.get("error")) + ) + else: + obj._update_data(content["success"]) + + if errors: + # TODO: how to raise list of errors? + # raise MultipleValidationErrors(errors) + # add test + raise errors[0] + + @classmethod + def _find_unsaved_children(cls, obj, children, files): + def callback(o): + if isinstance(o, Object): + if o.is_dirty(): + children.append(o) + return + + if isinstance(o, leancloud.File): + if not o.url or not o.id: + files.append(o) + return + + utils.traverse_object(obj, callback) + +
[文档] def is_dirty(self, attr=None): + # consider renaming to is_changed? + if attr: + return attr in self._changes + else: + return bool(not self.id or self._changes)
+ + def _to_pointer(self): + return { + "__type": "Pointer", + "className": self._class_name, + "objectId": self.id, + } + + def _merge_metadata(self, server_data): + object_id = server_data.get("objectId") + _created_at = utils.decode_date_string(server_data.get("createdAt")) + _updated_at = utils.decode_updated_at(server_data.get("updatedAt"), _created_at) + + if object_id is not None: + self.id = object_id + if _created_at is not None: + self.created_at = _created_at + if _updated_at is not None: + self.updated_at = _updated_at + + + +
[文档] def validate(self, attrs): + if "ACL" in attrs and not isinstance(attrs["ACL"], leancloud.ACL): + raise TypeError("acl must be a ACL") + return True
+ +
[文档] def get(self, attr, default=None, deafult=None): + """ + 获取对象字段的值 + + :param attr: 字段名 + :type attr: string_types + :return: 字段值 + """ + # for backward compatibility + if (deafult is not None) and (default is None): + default = deafult + + # createdAt is stored as string in the cloud but used as datetime object on the client side. + # We need to make sure that `.created_at` and `.get("createdAt")` return the same value. + # Otherwise users will get confused. + if attr == "createdAt": + if self.created_at is None: + return None + else: + return self.created_at + + # Similar to createdAt. + if attr == "updatedAt": + if self.updated_at is None: + return None + else: + return self.updated_at + + return self._attributes.get(attr, default)
+ +
[文档] def relation(self, attr): + """ + 返回对象上相应字段的 Relation + + :param attr: 字段名 + :type attr: string_types + :return: Relation + :rtype: leancloud.Relation + """ + value = self.get(attr) + if value is not None: + if not isinstance(value, leancloud.Relation): + raise TypeError("field %s is not Relation".format(attr)) + value._ensure_parent_and_key(self, attr) + return value + return leancloud.Relation(self, attr)
+ +
[文档] def has(self, attr): + """ + 判断此字段是否有值 + + :param attr: 字段名 + :return: 当有值时返回 True, 否则返回 False + :rtype: bool + """ + return attr in self._attributes
+ +
[文档] def set(self, key_or_attrs, value=None, unset=False): + """ + 在当前对象此字段上赋值 + + :param key_or_attrs: 字段名,或者一个包含 字段名 / 值的 dict + :type key_or_attrs: string_types or dict + :param value: 字段值 + :param unset: + :return: 当前对象,供链式调用 + """ + if isinstance(key_or_attrs, dict) and value is None: + attrs = key_or_attrs + keys = attrs.keys() + for k in keys: + if isinstance(attrs[k], LocalProxy): + attrs[k] = attrs[k]._get_current_object() + else: + key = key_or_attrs + if isinstance(value, LocalProxy): + value = value._get_current_object() + attrs = {key: utils.decode(key, value)} + + if unset: + for k in attrs.keys(): + attrs[k] = operation.Unset() + + self.validate(attrs) + + self._merge_metadata(attrs) + + keys = list(attrs.keys()) + for k in keys: + v = attrs[k] + # TODO: Relation + + if not isinstance(v, operation.BaseOp): + v = operation.Set(v) + + self._attributes[k] = v._apply(self._attributes.get(k), self, k) + if self._attributes[k] == operation._UNSET: + del self._attributes[k] + self._changes[k] = v._merge(self._changes.get(k)) + + return self
+ +
[文档] def unset(self, attr): + """ + 在对象上移除此字段。 + + :param attr: 字段名 + :return: 当前对象 + """ + return self.set(attr, None, unset=True)
+ +
[文档] def increment(self, attr, amount=1): + """ + 在对象此字段上自增对应的数值,如果数值没有指定,默认为一。 + + :param attr: 字段名 + :param amount: 自增量 + :return: 当前对象 + """ + return self.set(attr, operation.Increment(amount))
+ +
[文档] def add(self, attr, item): + """ + 在对象此字段对应的数组末尾添加指定对象。 + + :param attr: 字段名 + :param item: 要添加的对象 + :return: 当前对象 + """ + return self.set(attr, operation.Add([item]))
+ +
[文档] def add_unique(self, attr, item): + """ + 在对象此字段对应的数组末尾添加指定对象,如果此对象并没有包含在字段中。 + + :param attr: 字段名 + :param item: 要添加的对象 + :return: 当前对象 + """ + return self.set(attr, operation.AddUnique([item]))
+ +
[文档] def remove(self, attr, item): + """ + 在对象此字段对应的数组中,将指定对象全部移除。 + + :param attr: 字段名 + :param item: 要移除的对象 + :return: 当前对象 + """ + return self.set(attr, operation.Remove([item]))
+ +
[文档] def bit_and(self, attr, value): + return self.set(attr, operation.BitAnd(value))
+ +
[文档] def bit_or(self, attr, value): + return self.set(attr, operation.BitOr(value))
+ +
[文档] def bit_xor(self, attr, value): + return self.set(attr, operation.BitXor(value))
+ +
[文档] def clear(self): + """ + 将当前对象所有字段全部移除。 + + :return: 当前对象 + """ + self.set(self._attributes, unset=True)
+ + def _dump_save(self): + data = {k: v.dump() for k, v in six.iteritems(self._changes)} + data.update(self._flags) + return data + +
[文档] def fetch(self, select=None, include=None): + """ + 从服务器获取当前对象所有的值,如果与本地值不同,将会覆盖本地的值。 + + :return: 当前对象 + """ + data = {} + if select: + if not isinstance(select, (list, tuple)): + raise TypeError("select parameter must be a list or a tuple") + data["keys"] = ",".join(select) + if include: + if not isinstance(include, (list, tuple)): + raise TypeError("include parameter must be a list or a tuple") + data["include"] = ",".join(include) + response = client.get( + "/classes/{0}/{1}".format(self._class_name, self.id), data + ) + self._update_data(response.json())
+ +
[文档] def is_new(self): + """ + 判断当前对象是否已经保存至服务器。 + + 该方法为 SDK 内部使用(save 调用此方法 dispatch 保存操作为 REST API 的 POST 和 PUT 请求)。 + 查询对象是否在服务器上存在请使用 is_existed 方法。 + + + :rtype: bool + """ + return False if self.id else True
+ +
[文档] def is_existed(self): + """ + 判断当前对象是否在服务器上已经存在。 + + :rtype: bool + """ + return self.has("createdAt")
+ +
[文档] def get_acl(self): + """ + 返回当前对象的 ACL。 + + :return: 当前对象的 ACL + :rtype: leancloud.ACL + """ + return self.get("ACL")
+ +
[文档] def set_acl(self, acl): + """ + 为当前对象设置 ACL + + :type acl: leancloud.ACL + :return: 当前对象 + """ + + return self.set("ACL", acl)
+ +
[文档] def disable_before_hook(self): + hook_key = client.get_app_info().get("hook_key") + master_key = client.get_app_info().get("master_key") + if hook_key or master_key: + self.ignore_hook("beforeSave") + self.ignore_hook("beforeUpdate") + self.ignore_hook("beforeDelete") + return self + else: + raise ValueError("disable_before_hook needs master key or hook key")
+ +
[文档] def disable_after_hook(self): + hook_key = client.get_app_info().get("hook_key") + master_key = client.get_app_info().get("master_key") + if hook_key or master_key: + self.ignore_hook("afterSave") + self.ignore_hook("afterUpdate") + self.ignore_hook("afterDelete") + return self + else: + raise ValueError("disable_after_hook needs master key or hook key")
+ +
[文档] def ignore_hook(self, hook_name): + if hook_name not in { + "beforeSave", + "afterSave", + "beforeUpdate", + "afterUpdate", + "beforeDelete", + "afterDelete", + }: + raise ValueError("invalid hook name: " + hook_name) + if "__ignore_hooks" not in self._flags: + self._flags["__ignore_hooks"] = [] + self._flags["__ignore_hooks"].append(hook_name)
+ + def _update_data(self, server_data): + self._merge_metadata(server_data) + for key, value in six.iteritems(server_data): + self._attributes[key] = utils.decode(key, value) + self._changes = {} + +
[文档] @staticmethod + def as_class(arg): + def inner_decorator(cls): + cls._class_name = arg + return cls + + return inner_decorator
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/push.html b/docs/_modules/leancloud/push.html new file mode 100644 index 00000000..78697b4a --- /dev/null +++ b/docs/_modules/leancloud/push.html @@ -0,0 +1,206 @@ + + + + + + leancloud.push — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.push 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import arrow
+import dateutil.tz as tz
+
+from leancloud.object_ import Object
+from leancloud.errors import LeanCloudError
+from leancloud import client
+
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+
[文档]class Installation(Object): + pass
+ + +
[文档]class Notification(Object): +
[文档] def fetch(self, *args, **kwargs): + """同步服务器的 Notification 数据 + """ + response = client.get("/tables/Notifications/{0}".format(self.id)) + self._update_data(response.json())
+ +
[文档] def save(self, *args, **kwargs): + raise LeanCloudError(code=1, error="Notification does not support modify")
+ + +def _encode_time(time): + tzinfo = time.tzinfo + if tzinfo is None: + tzinfo = tz.tzlocal() + return arrow.get(time, tzinfo).to("utc").format("YYYY-MM-DDTHH:mm:ss.SSS") + "Z" + + +
[文档]def send( + data, + channels=None, + push_time=None, + expiration_time=None, + expiration_interval=None, + where=None, + cql=None, + flow_control=None, + prod=None, +): + """ + 发送推送消息。返回结果为此条推送对应的 _Notification 表中的对象,但是如果需要使用其中的数据,需要调用 fetch() 方法将数据同步至本地。 + + :param channels: 需要推送的频道 + :type channels: list or tuple + :param push_time: 推送的时间 + :type push_time: datetime + :param expiration_time: 消息过期的绝对日期时间 + :type expiration_time: datetime + :param expiration_interval: 消息过期的相对时间,从调用 API 的时间开始算起,单位是秒 + :type expiration_interval: int + :param where: 一个查询 _Installation 表的查询条件 leancloud.Query 对象 + :type where: leancloud.Query + :param cql: 一个查询 _Installation 表的查询条件 CQL 语句 + :type cql: string_types + :param data: 推送给设备的具体信息,详情查看 https://leancloud.cn/docs/push_guide.html#消息内容_Data + :rtype: Notification + :param flow_control: 不为 None 时开启平滑推送,值为每秒推送的目标终端用户数。开启时指定低于 1000 的值,按 1000 计。 + :type: flow_control: int + :param prod: 仅对 iOS 推送有效,设置将推送发至 APNs 的开发环境(dev)还是生产环境(prod)。 + :type: prod: string + """ + if expiration_interval and expiration_time: + raise TypeError("Both expiration_time and expiration_interval can't be set") + + params = { + "data": data, + } + + if prod is None: + if client.USE_PRODUCTION == "0": + params["prod"] = "dev" + else: + params["prod"] = prod + + if channels: + params["channels"] = channels + if push_time: + params["push_time"] = _encode_time(push_time) + if expiration_time: + params["expiration_time"] = _encode_time(expiration_time) + if expiration_interval: + params["expiration_interval"] = expiration_interval + if where: + params["where"] = where.dump().get("where", {}) + if cql: + params["cql"] = cql + # Do not change this to `if flow_control`, because 0 is falsy in Python, + # but `flow_control = 0` will enable smooth push, + # and it is in fact equivalent to `flow_control = 1000`. + if flow_control is not None: + params["flow_control"] = flow_control + + result = client.post("/push", params=params).json() + + notification = Notification.create_without_data(result["objectId"]) + return notification
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/query.html b/docs/_modules/leancloud/query.html new file mode 100644 index 00000000..989ea398 --- /dev/null +++ b/docs/_modules/leancloud/query.html @@ -0,0 +1,819 @@ + + + + + + leancloud.query — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.query 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import json
+
+import six
+
+import leancloud
+from leancloud import client
+from leancloud import utils
+from leancloud.file_ import File
+from leancloud.object_ import Object
+from leancloud.errors import LeanCloudError
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+class CQLResult(object):
+    """
+    CQL 查询结果对象。
+
+    Attributes:
+        results: 返回的查询结果
+
+        count: 如果查询语句包含 count,将保存在此字段
+
+        class_name: 查询的 class 名称
+    """
+
+    __slots__ = ["results", "count", "class_name"]
+
+    def __init__(self, results, count, class_name):
+        self.results = results
+        self.count = count
+        self.class_name = class_name
+
+
+class Cursor(object):
+    """
+    Query.scan 返回结果对象。
+    """
+
+    def __init__(self, query_class, batch_size, scan_key, params):
+        self._params = params
+        self._query_class = query_class
+
+        if batch_size is not None:
+            self._params["limit"] = batch_size
+
+        if scan_key is not None:
+            self._params["scan_key"] = scan_key
+
+    def __iter__(self):
+        while True:
+            content = client.get(
+                "/scan/classes/{}".format(self._query_class._class_name), self._params
+            ).json()
+            for result in content["results"]:
+                obj = self._query_class()
+                obj._update_data(result)
+                yield obj
+
+            if not content.get("cursor"):
+                break
+
+            self._params["cursor"] = content["cursor"]
+
+
+
[文档]class Query(object): + def __init__(self, query_class): + """ + + :param query_class: 要查询的 class 名称或者对象 + :type query_class: string_types or leancloud.ObjectMeta + """ + if isinstance(query_class, six.string_types): + if query_class in ("File", "_File"): + query_class = File + else: + query_class = Object.extend(query_class) + + if not isinstance(query_class, (type, six.class_types)) or not issubclass( + query_class, (File, Object) + ): + raise ValueError("Query takes string or LeanCloud Object") + + self._query_class = query_class + + self._where = {} + self._include = [] + self._include_acl = None + self._limit = -1 + self._skip = 0 + self._extra = {} + self._order = [] + self._select = [] + +
[文档] @classmethod + def or_(cls, *queries): + """ + 根据传入的 Query 对象,构造一个新的 OR 查询。 + + :param queries: 需要构造的子查询列表 + :rtype: Query + """ + if len(queries) < 2: + raise ValueError("or_ need two queries at least") + if not all( + x._query_class._class_name == queries[0]._query_class._class_name + for x in queries + ): + raise TypeError("All queries must be for the same class") + query = Query(queries[0]._query_class._class_name) + query._or_query(queries) + return query
+ +
[文档] @classmethod + def and_(cls, *queries): + """ + 根据传入的 Query 对象,构造一个新的 AND 查询。 + + :param queries: 需要构造的子查询列表 + :rtype: Query + """ + if len(queries) < 2: + raise ValueError("and_ need two queries at least") + if not all( + x._query_class._class_name == queries[0]._query_class._class_name + for x in queries + ): + raise TypeError("All queries must be for the same class") + query = Query(queries[0]._query_class._class_name) + query._and_query(queries) + return query
+ +
[文档] @classmethod + def do_cloud_query(cls, cql, *pvalues): + """ + 使用 CQL 来构造查询。CQL 语法参考 `这里 <https://cn.avoscloud.com/docs/cql_guide.html>`_。 + + :param cql: CQL 语句 + :param pvalues: 查询参数 + :rtype: CQLResult + """ + params = {"cql": cql} + if len(pvalues) == 1 and isinstance(pvalues[0], (tuple, list)): + pvalues = json.dumps(pvalues[0]) + if len(pvalues) > 0: + params["pvalues"] = json.dumps(pvalues) + + content = client.get("/cloudQuery", params).json() + + objs = [] + query = cls(content["className"]) + for result in content["results"]: + obj = query._new_object() + obj._update_data(query._process_result(result)) + objs.append(obj) + + return CQLResult(objs, content.get("count"), content.get("className"))
+ +
[文档] def dump(self): + """ + :return: 当前对象的序列化结果 + :rtype: dict + """ + params = { + "where": self._where, + } + if self._include: + params["include"] = ",".join(self._include) + if self._select: + params["keys"] = ",".join(self._select) + if self._include_acl is not None: + params["returnACL"] = json.dumps(self._include_acl) + if self._limit >= 0: + params["limit"] = self._limit + if self._skip > 0: + params["skip"] = self._skip + if self._order: + params["order"] = ",".join(self._order) + params.update(self._extra) + return params
+ + def _new_object(self): + return self._query_class() + + def _process_result(self, obj): + return obj + + def _do_request(self, params): + return client.get( + "/classes/{0}".format(self._query_class._class_name), params + ).json() + +
[文档] def first(self): + """ + 根据查询获取最多一个对象。 + + :return: 查询结果 + :rtype: Object + :raise: LeanCloudError + """ + params = self.dump() + params["limit"] = 1 + content = self._do_request(params) + results = content["results"] + if not results: + raise LeanCloudError(101, "Object not found") + obj = self._new_object() + obj._update_data(self._process_result(results[0])) + return obj
+ +
[文档] def get(self, object_id): + """ + 根据 objectId 查询。 + + :param object_id: 要查询对象的 objectId + :return: 查询结果 + :rtype: Object + """ + if not object_id: + raise LeanCloudError(code=101, error="Object not found.") + obj = self._query_class.create_without_data(object_id) + obj.fetch(select=self._select, include=self._include) + return obj
+ +
[文档] def find(self): + """ + 根据查询条件,获取包含所有满足条件的对象。 + + :rtype: list + """ + content = self._do_request(self.dump()) + + objs = [] + for result in content["results"]: + obj = self._new_object() + obj._update_data(self._process_result(result)) + objs.append(obj) + + return objs
+ +
[文档] def scan(self, batch_size=None, scan_key=None): + params = self.dump() + if "skip" in params: + raise LeanCloudError(1, "Query.scan dose not support skip option") + if "limit" in params: + raise LeanCloudError(1, "Query.scan dose not support limit option") + return Cursor(self._query_class, batch_size, scan_key, params)
+ +
[文档] def count(self): + """ + 返回满足查询条件的对象的数量。 + + :rtype: int + """ + params = self.dump() + params["limit"] = 0 + params["count"] = 1 + content = self._do_request(params) + return content["count"]
+ +
[文档] def skip(self, n): + """ + 查询条件中跳过指定个数的对象,在做分页时很有帮助。 + + :param n: 需要跳过对象的个数 + :rtype: Query + """ + self._skip = n + return self
+ +
[文档] def limit(self, n): + """ + 设置查询返回结果的数量。如果不设置,默认为 100。最大返回数量为 1000,如果超过这个数量,需要使用多次查询来获取结果。 + + :param n: 限制结果的数量 + :rtype: Query + """ + if n > 1000: + raise ValueError("limit only accept number less than or equal to 1000") + self._limit = n + return self
+ +
[文档] def include_acl(self, value=True): + """ + 设置查询结果的对象,是否包含 ACL 字段。需要在控制台选项中开启对应选项才能生效。 + + :param value: 是否包含 ACL,默认为 True + :type value: bool + :rtype: Query + """ + self._include_acl = value + return self
+ +
[文档] def equal_to(self, key, value): + """ + 增加查询条件,查询字段的值必须为指定值。 + + :param key: 查询条件的字段名 + :param value: 查询条件的值 + :rtype: Query + """ + self._where[key] = utils.encode(value) + return self
+ +
[文档] def size_equal_to(self, key, size): + """ + 增加查询条件,限制查询结果指定数组字段长度与查询值相同 + + :param key: 查询条件数组字段名 + :param size: 查询条件值 + :rtype: Query + """ + self._add_condition(key, "$size", size) + return self
+ + def _add_condition(self, key, condition, value): + if not self._where.get(key): + self._where[key] = {} + self._where[key][condition] = utils.encode(value) + return self + +
[文档] def not_equal_to(self, key, value): + """ + 增加查询条件,限制查询结果指定字段的值与查询值不同 + + :param key: 查询条件字段名 + :param value: 查询条件值 + :rtype: Query + """ + self._add_condition(key, "$ne", value) + return self
+ +
[文档] def less_than(self, key, value): + """ + 增加查询条件,限制查询结果指定字段的值小于查询值 + + :param key: 查询条件字段名 + :param value: 查询条件值 + :rtype: Query + """ + self._add_condition(key, "$lt", value) + return self
+ +
[文档] def greater_than(self, key, value): + """ + 增加查询条件,限制查询结果指定字段的值大于查询值 + + :param key: 查询条件字段名 + :param value: 查询条件值 + :rtype: Query + """ + self._add_condition(key, "$gt", value) + return self
+ +
[文档] def less_than_or_equal_to(self, key, value): + """ + 增加查询条件,限制查询结果指定字段的值小于等于查询值 + + :param key: 查询条件字段名 + :param value: 查询条件值 + :rtype: Query + """ + self._add_condition(key, "$lte", value) + return self
+ +
[文档] def greater_than_or_equal_to(self, key, value): + """ + 增加查询条件,限制查询结果指定字段的值大于等于查询值 + + :param key: 查询条件字段名 + :param value: 查询条件值名 + :rtype: Query + """ + self._add_condition(key, "$gte", value) + return self
+ +
[文档] def contained_in(self, key, values): + """ + 增加查询条件,限制查询结果指定字段的值在查询值列表中 + + :param key: 查询条件字段名 + :param values: 查询条件值 + :type values: list or tuple + :rtype: Query + """ + self._add_condition(key, "$in", values) + return self
+ +
[文档] def not_contained_in(self, key, values): + """ + 增加查询条件,限制查询结果指定字段的值不在查询值列表中 + + :param key: 查询条件字段名 + :param values: 查询条件值 + :type values: list or tuple + :rtype: Query + """ + self._add_condition(key, "$nin", values) + return self
+ +
[文档] def contains_all(self, key, values): + """ + 增加查询条件,限制查询结果指定字段的值全部包含与查询值列表中 + + :param key: 查询条件字段名 + :param values: 查询条件值 + :type values: list or tuple + :rtype: Query + """ + self._add_condition(key, "$all", values) + return self
+ +
[文档] def exists(self, key): + """ + 增加查询条件,限制查询结果对象包含指定字段 + + :param key: 查询条件字段名 + :rtype: Query + """ + self._add_condition(key, "$exists", True) + return self
+ +
[文档] def does_not_exist(self, key): + """ + 增加查询条件,限制查询结果对象不包含指定字段 + + :param key: 查询条件字段名 + :rtype: Query + """ + self._add_condition(key, "$exists", False) + return self
+ +
[文档] def matched(self, key, regex, ignore_case=False, multi_line=False): + """ + 增加查询条件,限制查询结果对象指定字段满足指定的正则表达式。 + + :param key: 查询条件字段名 + :param regex: 查询正则表达式 + :param ignore_case: 查询是否忽略大小写,默认不忽略 + :param multi_line: 查询是否匹配多行,默认不匹配 + :rtype: Query + """ + if not isinstance(regex, six.string_types): + raise TypeError("matched only accept str or unicode") + self._add_condition(key, "$regex", regex) + modifiers = "" + if ignore_case: + modifiers += "i" + if multi_line: + modifiers += "m" + if modifiers: + self._add_condition(key, "$options", modifiers) + return self
+ +
[文档] def matches_query(self, key, query): + """ + 增加查询条件,限制查询结果对象指定字段的值,与另外一个查询对象的返回结果相同。 + + :param key: 查询条件字段名 + :param query: 查询对象 + :type query: Query + :rtype: Query + """ + dumped = query.dump() + dumped["className"] = query._query_class._class_name + self._add_condition(key, "$inQuery", dumped) + return self
+ +
[文档] def does_not_match_query(self, key, query): + """ + 增加查询条件,限制查询结果对象指定字段的值,与另外一个查询对象的返回结果不相同。 + + :param key: 查询条件字段名 + :param query: 查询对象 + :type query: Query + :rtype: Query + """ + dumped = query.dump() + dumped["className"] = query._query_class._class_name + self._add_condition(key, "$notInQuery", dumped) + return self
+ +
[文档] def matches_key_in_query(self, key, query_key, query): + """ + 增加查询条件,限制查询结果对象指定字段的值,与另外一个查询对象的返回结果指定的值相同。 + + :param key: 查询条件字段名 + :param query_key: 查询对象返回结果的字段名 + :param query: 查询对象 + :type query: Query + :rtype: Query + """ + dumped = query.dump() + dumped["className"] = query._query_class._class_name + self._add_condition(key, "$select", {"key": query_key, "query": dumped}) + return self
+ +
[文档] def does_not_match_key_in_query(self, key, query_key, query): + """ + 增加查询条件,限制查询结果对象指定字段的值,与另外一个查询对象的返回结果指定的值不相同。 + + :param key: 查询条件字段名 + :param query_key: 查询对象返回结果的字段名 + :param query: 查询对象 + :type query: Query + :rtype: Query + """ + dumped = query.dump() + dumped["className"] = query._query_class._class_name + self._add_condition(key, "$dontSelect", {"key": query_key, "query": dumped}) + return self
+ + def _or_query(self, queries): + dumped = [q.dump()["where"] for q in queries] + self._where["$or"] = dumped + return self + + def _and_query(self, queries): + dumped = [q.dump()["where"] for q in queries] + self._where["$and"] = dumped + + def _quote(self, s): + # return "\\Q" + s.replace("\\E", "\\E\\\\E\\Q") + "\\E" + return s + +
[文档] def contains(self, key, value): + """ + 增加查询条件,限制查询结果对象指定最短的值,包含指定字符串。在数据量比较大的情况下会比较慢。 + + :param key: 查询条件字段名 + :param value: 需要包含的字符串 + :rtype: Query + """ + self._add_condition(key, "$regex", self._quote(value)) + return self
+ +
[文档] def startswith(self, key, value): + """ + 增加查询条件,限制查询结果对象指定最短的值,以指定字符串开头。在数据量比较大的情况下会比较慢。 + + :param key: 查询条件字段名 + :param value: 需要查询的字符串 + :rtype: Query + """ + value = value if isinstance(value, six.text_type) else value.decode("utf-8") + self._add_condition(key, "$regex", "^" + self._quote(value)) + return self
+ +
[文档] def endswith(self, key, value): + """ + 增加查询条件,限制查询结果对象指定最短的值,以指定字符串结尾。在数据量比较大的情况下会比较慢。 + + :param key: 查询条件字段名 + :param value: 需要查询的字符串 + :rtype: Query + """ + value = value if isinstance(value, six.text_type) else value.decode("utf-8") + self._add_condition(key, "$regex", self._quote(value) + "$") + return self
+ +
[文档] def ascending(self, key): + """ + 限制查询返回结果以指定字段升序排序。 + + :param key: 排序字段名 + :rtype: Query + """ + self._order = [key] + return self
+ +
[文档] def add_ascending(self, key): + """ + 增加查询排序条件。之前指定的排序条件优先级更高。 + + :param key: 排序字段名 + :rtype: Query + """ + self._order.append(key) + return self
+ +
[文档] def descending(self, key): + """ + 限制查询返回结果以指定字段降序排序。 + + :param key: 排序字段名 + :rtype: Query + """ + self._order = ["-{0}".format(key)] + return self
+ +
[文档] def add_descending(self, key): + """ + 增加查询排序条件。之前指定的排序条件优先级更高。 + + :param key: 排序字段名 + :rtype: Query + """ + self._order.append("-{0}".format(key)) + return self
+ +
[文档] def near(self, key, point): + """ + 增加查询条件,限制返回结果指定字段值的位置与给定地理位置临近。 + + :param key: 查询条件字段名 + :param point: 需要查询的地理位置 + :rtype: Query + """ + if point is None: + raise ValueError("near query does not accept None") + + self._add_condition(key, "$nearSphere", point) + return self
+ +
[文档] def within_radians(self, key, point, max_distance, min_distance=None): + """ + 增加查询条件,限制返回结果指定字段值的位置在某点的一段距离之内。 + + :param key: 查询条件字段名 + :param point: 查询地理位置 + :param max_distance: 最大距离限定(弧度) + :param min_distance: 最小距离限定(弧度) + :rtype: Query + """ + self.near(key, point) + self._add_condition(key, "$maxDistance", max_distance) + if min_distance is not None: + self._add_condition(key, "$minDistance", min_distance) + return self
+ +
[文档] def within_miles(self, key, point, max_distance, min_distance=None): + """ + 增加查询条件,限制返回结果指定字段值的位置在某点的一段距离之内。 + + :param key: 查询条件字段名 + :param point: 查询地理位置 + :param max_distance: 最大距离限定(英里) + :param min_distance: 最小距离限定(英里) + :rtype: Query + """ + if min_distance is not None: + min_distance = min_distance / 3958.8 + return self.within_radians(key, point, max_distance / 3958.8, min_distance)
+ +
[文档] def within_kilometers(self, key, point, max_distance, min_distance=None): + """ + 增加查询条件,限制返回结果指定字段值的位置在某点的一段距离之内。 + + :param key: 查询条件字段名 + :param point: 查询地理位置 + :param max_distance: 最大距离限定(千米) + :param min_distance: 最小距离限定(千米) + :rtype: Query + """ + if min_distance is not None: + min_distance = min_distance / 6371.0 + return self.within_radians(key, point, max_distance / 6371.0, min_distance)
+ +
[文档] def within_geo_box(self, key, southwest, northeast): + """ + 增加查询条件,限制返回结果指定字段值的位置在指定坐标范围之内。 + + :param key: 查询条件字段名 + :param southwest: 限制范围西南角坐标 + :param northeast: 限制范围东北角坐标 + :rtype: Query + """ + self._add_condition(key, "$within", {"$box": [southwest, northeast]}) + return self
+ +
[文档] def include(self, *keys): + """ + 指定查询返回结果中包含关联表字段。 + + :param keys: 关联子表字段名 + :rtype: Query + """ + if len(keys) == 1 and isinstance(keys[0], (list, tuple)): + keys = keys[0] + self._include += keys + return self
+ +
[文档] def select(self, *keys): + """ + 指定查询返回结果中只包含某些字段。可以重复调用,每次调用的包含内容都将会被返回。 + + :param keys: 包含字段名 + :rtype: Query + """ + if len(keys) == 1 and isinstance(keys[0], (list, tuple)): + keys = keys[0] + self._select += keys + return self
+ + +
[文档]class FriendshipQuery(Query): + def __init__(self, query_class): + super(FriendshipQuery, self).__init__(query_class) + if query_class in ("_Follower", "Follower"): + self._friendship_tag = "follower" + elif query_class in ("_Followee", "Followee"): + self._friendship_tag = "followee" + else: + raise TypeError("FriendshipQuery takes only follower or followee") + + def _new_object(self): + return leancloud.User() + + def _process_result(self, obj): + content = obj[self._friendship_tag] + if content["__type"] == "Pointer" and content["className"] == "_User": + del content["__type"] + del content["className"] + return content
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/relation.html b/docs/_modules/leancloud/relation.html new file mode 100644 index 00000000..1e1cc23a --- /dev/null +++ b/docs/_modules/leancloud/relation.html @@ -0,0 +1,192 @@ + + + + + + leancloud.relation — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.relation 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import leancloud
+from leancloud import operation
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+
[文档]class Relation(object): + def __init__(self, parent, key=None): + self.parent = parent + self.key = key + self.target_class_name = None + +
[文档] @classmethod + def reverse_query(cls, parent_class, relation_key, child): + """ + 创建一个新的 Query 对象,反向查询所有指向此 Relation 的父对象。 + + :param parent_class: 父类名称 + :param relation_key: 父类中 Relation 的字段名 + :param child: 子类对象 + :return: leancloud.Query + """ + q = leancloud.Query(parent_class) + q.equal_to(relation_key, child._to_pointer()) + return q
+ + def _ensure_parent_and_key(self, parent=None, key=None): + if self.parent is None: + self.parent = parent + if self.key is None: + self.key = key + + if self.parent != parent: + raise TypeError("relation retrieved from two different object") + if self.key != key: + raise TypeError("relation retrieved from two different object") + +
[文档] def add(self, *obj_or_objs): + """ + 添加一个新的 leancloud.Object 至 Relation。 + + :param obj_or_objs: 需要添加的对象或对象列表 + """ + objs = obj_or_objs + if not isinstance(obj_or_objs, (list, tuple)): + objs = (obj_or_objs,) + change = operation.Relation(objs, ()) + self.parent.set(self.key, change) + self.target_class_name = change._target_class_name
+ +
[文档] def remove(self, *obj_or_objs): + """ + 从一个 Relation 中删除一个 leancloud.Object 。 + + :param obj_or_objs: 需要删除的对象或对象列表 + :return: + """ + objs = obj_or_objs + if not isinstance(obj_or_objs, (list, tuple)): + objs = (obj_or_objs,) + change = operation.Relation((), objs) + self.parent.set(self.key, change) + self.target_class_name = change._target_class_name
+ +
[文档] def dump(self): + return {"__type": "Relation", "className": self.target_class_name}
+ + @property + def query(self): + """ + 获取指向 Relation 内容的 Query 对象。 + + :rtype: leancloud.Query + """ + + if self.target_class_name is None: + target_class = leancloud.Object.extend(self.parent._class_name) + query = leancloud.Query(target_class) + query._extra["redirectClassNameForKey"] = self.key + else: + target_class = leancloud.Object.extend(self.target_class_name) + query = leancloud.Query(target_class) + + query._add_condition("$relatedTo", "object", self.parent._to_pointer()) + query._add_condition("$relatedTo", "key", self.key) + + return query
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/role.html b/docs/_modules/leancloud/role.html new file mode 100644 index 00000000..9e8a702d --- /dev/null +++ b/docs/_modules/leancloud/role.html @@ -0,0 +1,175 @@ + + + + + + leancloud.role — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.role 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import re
+
+import six
+
+import leancloud
+
+__author__ = "asaka <lan@leancloud.rocks>"
+
+
+
[文档]class Role(leancloud.Object): + def __init__(self, name=None, acl=None): + super(Role, self).__init__() + if name: + self.set_name(name) + if acl is None: + acl = leancloud.ACL() + acl.set_public_read_access(True) + self.set_acl(acl) + + @property + def name(self): + return self.get("name") + + @name.setter + def name(self, name): + return self.set("name", name) + +
[文档] def get_name(self): + """ + 获取 Role 的 name,等同于 role.get('name') + """ + return self.get("name")
+ +
[文档] def set_name(self, name): + """ + 为 Role 设置 name,等同于 role.set('name', name) + """ + return self.set("name", name)
+ + @property + def users(self): + return self.relation("users") + +
[文档] def get_users(self): + """ + 获取当前 Role 下所有绑定的用户。 + """ + return self.relation("users")
+ + @property + def roles(self): + return self.relation("roles") + +
[文档] def get_roles(self): + return self.relation("roles")
+ +
[文档] def validate(self, attrs): + if "name" in attrs and attrs["name"] != self.get_name(): + new_name = attrs["name"] + if not isinstance(new_name, six.string_types): + raise TypeError("role name must be string_types") + r = re.compile(r"^[0-9a-zA-Z\-_]+$") + if not r.match(new_name): + raise TypeError( + """ + role's name can only contain alphanumeric characters, _, -, and spaces. + """ + ) + + return super(Role, self).validate(attrs)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_modules/leancloud/user.html b/docs/_modules/leancloud/user.html new file mode 100644 index 00000000..f3d31e16 --- /dev/null +++ b/docs/_modules/leancloud/user.html @@ -0,0 +1,468 @@ + + + + + + leancloud.user — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

leancloud.user 源代码

+# coding: utf-8
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+
+import threading
+from typing import Optional
+
+import six
+
+from leancloud import client
+from leancloud.errors import LeanCloudError
+from leancloud.query import FriendshipQuery
+from leancloud.object_ import Object
+from leancloud.relation import Relation
+
+__author__ = "asaka"
+
+
+thread_locals = threading.local()
+thread_locals.current_user = None
+
+
+
[文档]class User(Object): + def __init__(self, **attrs): + self._session_token = None + super(User, self).__init__(**attrs) + +
[文档] def get_session_token(self): + return self._session_token
+ + @property + def session_token(self): + return self._session_token + + def _merge_metadata(self, attrs): + if "sessionToken" in attrs: + self._session_token = attrs.pop("sessionToken") + + return super(User, self)._merge_metadata(attrs) + +
[文档] @classmethod + def create_follower_query(cls, user_id): + if not user_id or not isinstance(user_id, six.string_types): + raise TypeError("invalid user_id: {0}".format(user_id)) + query = FriendshipQuery("_Follower") + query.equal_to("user", User.create_without_data(user_id)) + return query
+ +
[文档] @classmethod + def create_followee_query(cls, user_id): + if not user_id or not isinstance(user_id, six.string_types): + raise TypeError("invalid user_id: {0}".format(user_id)) + query = FriendshipQuery("_Followee") + query.equal_to("user", User.create_without_data(user_id)) + return query
+ +
[文档] @classmethod + def get_current(cls): # type: () -> Optional[User] + return getattr(thread_locals, "current_user", None)
+ +
[文档] @classmethod + def set_current(cls, user): + thread_locals.current_user = user
+ +
[文档] @classmethod + def become(cls, session_token): + """ + 通过 session token 获取用户对象 + + :param session_token: 用户的 session token + :return: leancloud.User + """ + response = client.get("/users/me", params={"session_token": session_token}) + content = response.json() + user = cls() + user._update_data(content) + user._handle_save_result(True) + if "smsCode" not in content: + user._attributes.pop("smsCode", None) + return user
+ + @property + def is_current(self): + if not getattr(thread_locals, "current_user", None): + return False + return self.id == thread_locals.current_user.id + + def _cleanup_auth_data(self): + if not self.is_current: + return + auth_data = self.get("authData") + if not auth_data: + return + keys = list(auth_data.keys()) + for key in keys: + if not auth_data[key]: + del auth_data[key] + + def _handle_save_result(self, make_current=False): + if make_current: + User.set_current(self) + self._cleanup_auth_data() + # self._sync_all_auth_data() + self._attributes.pop("password", None) + +
[文档] def save(self, make_current=False): + super(User, self).save() + self._handle_save_result(make_current)
+ +
[文档] def sign_up(self, username=None, password=None): + """ + 创建一个新用户。新创建的 User 对象,应该使用此方法来将数据保存至服务器,而不是使用 save 方法。 + 用户对象上必须包含 username 和 password 两个字段 + """ + if username: + self.set("username", username) + if password: + self.set("password", password) + + username = self.get("username") + if not username: + raise TypeError("invalid username: {0}".format(username)) + password = self.get("password") + if not password: + raise TypeError("invalid password") + + self.save(make_current=True)
+ +
[文档] def login(self, username=None, password=None, email=None): + """ + 登录用户。成功登录后,服务器会返回用户的 sessionToken 。 + + :param username: 用户名 + :param email: 邮箱地址(username 和 email 这两个参数必须传入一个且仅能传入一个) + :param password: 用户密码 + """ + if username: + self.set("username", username) + if password: + self.set("password", password) + if email: + self.set("email", email) + # 同时传入 username、email、password 的情况下,这三个字段会一起发给后端。 + # 这时后端会忽略 email,等价于只传 username 和 password。 + # 这里的 login 函数的实现依赖后端的这一行为,没有校验 username 和 email 中调用者传入且仅传入了其中一个参数。 + response = client.post("/login", params=self.dump()) + content = response.json() + self._update_data(content) + self._handle_save_result(True) + if "smsCode" not in content: + self._attributes.pop("smsCode", None)
+ +
[文档] def logout(self): + if not self.is_current: + return + self._cleanup_auth_data() + del thread_locals.current_user
+ +
[文档] @classmethod + def login_with_mobile_phone(cls, phone_number, password): + user = User() + params = {"mobilePhoneNumber": phone_number, "password": password} + user._update_data(params) + user.login() + return user
+ +
[文档] def follow(self, target_id): + """ + 关注一个用户。 + + :param target_id: 需要关注的用户的 id + """ + if self.id is None: + raise ValueError("Please sign in") + response = client.post( + "/users/{0}/friendship/{1}".format(self.id, target_id), None + ) + assert response.ok
+ +
[文档] def unfollow(self, target_id): + """ + 取消关注一个用户。 + + :param target_id: 需要关注的用户的 id + :return: + """ + if self.id is None: + raise ValueError("Please sign in") + response = client.delete( + "/users/{0}/friendship/{1}".format(self.id, target_id), None + ) + assert response.ok
+ +
[文档] @classmethod + def login_with(cls, platform, third_party_auth_data): + """ + 把第三方平台号绑定到 User 上 + + :param platform: 第三方平台名称 base string + """ + user = User() + return user.link_with(platform, third_party_auth_data)
+ + + + + +
[文档] def is_linked(self, provider): + try: + self.get("authData")[provider] + except KeyError: + return False + return True
+ +
[文档] @classmethod + def signup_or_login_with_mobile_phone(cls, phone_number, sms_code): + """ + param phone_nubmer: string_types + param sms_code: string_types + + 在调用此方法前请先使用 request_sms_code 请求 sms code + """ + data = {"mobilePhoneNumber": phone_number, "smsCode": sms_code} + response = client.post("/usersByMobilePhone", data) + content = response.json() + user = cls() + user._update_data(content) + user._handle_save_result(True) + if "smsCode" not in content: + user._attributes.pop("smsCode", None) + return user
+ +
[文档] def update_password(self, old_password, new_password): + route = "/users/" + self.id + "/updatePassword" + params = {"old_password": old_password, "new_password": new_password} + content = client.put(route, params).json() + self._update_data(content) + self._handle_save_result(True)
+ +
[文档] def get_username(self): + return self.get("username")
+ +
[文档] def get_mobile_phone_number(self): + return self.get("mobilePhoneNumber")
+ +
[文档] def set_mobile_phone_number(self, phone_number): + return self.set("mobilePhoneNumber", phone_number)
+ +
[文档] def set_username(self, username): + return self.set("username", username)
+ +
[文档] def set_password(self, password): + return self.set("password", password)
+ +
[文档] def set_email(self, email): + return self.set("email", email)
+ +
[文档] def get_email(self): + return self.get("email")
+ +
[文档] def get_roles(self): + return Relation.reverse_query("_Role", "users", self).find()
+ +
[文档] def refresh_session_token(self): + """ + 重置当前用户 `session token`。 + 会使其他客户端已登录用户登录失效。 + """ + response = client.put("/users/{}/refreshSessionToken".format(self.id), None) + content = response.json() + self._update_data(content) + self._handle_save_result(False)
+ +
[文档] def is_authenticated(self): + """ + 判断当前用户对象是否已登录。 + 会先检查此用户对象上是否有 `session_token`,如果有的话,会继续请求服务器验证 `session_token` 是否合法。 + """ + session_token = self.get_session_token() + if not session_token: + return False + try: + response = client.get("/users/me", params={"session_token": session_token}) + except LeanCloudError as e: + if e.code == 211: + return False + else: + raise + return response.status_code == 200
+ +
[文档] @classmethod + def request_password_reset(cls, email): + params = {"email": email} + client.post("/requestPasswordReset", params)
+ +
[文档] @classmethod + def request_email_verify(cls, email): + params = {"email": email} + client.post("/requestEmailVerify", params)
+ +
[文档] @classmethod + def request_mobile_phone_verify(cls, phone_number, validate_token=None): + params = {"mobilePhoneNumber": phone_number} + if validate_token is not None: + params["validate_token"] = validate_token + client.post("/requestMobilePhoneVerify", params)
+ +
[文档] @classmethod + def request_password_reset_by_sms_code(cls, phone_number, validate_token=None): + params = {"mobilePhoneNumber": phone_number} + if validate_token is not None: + params["validate_token"] = validate_token + client.post("/requestPasswordResetBySmsCode", params)
+ +
[文档] @classmethod + def reset_password_by_sms_code(cls, sms_code, new_password): + params = {"password": new_password} + client.put("/resetPasswordBySmsCode/" + sms_code, params)
+ + # This should be an instance method. + # However, to be consistent with other similar methods (`request_password_reset_by_sms_code`), + # it is implemented as a class method. +
[文档] @classmethod + def request_change_phone_number(cls, phone_number, ttl=None, validate_token=None): + params = {"mobilePhoneNumber": phone_number} + if ttl is not None: + params["ttl"] = ttl + if validate_token is not None: + params["validate_token"] = validate_token + client.post("/requestChangePhoneNumber", params)
+ + # This should be an instance method and update the local date, + # but it is implemented as a class method for the same reason as above. +
[文档] @classmethod + def change_phone_number(cls, sms_code, phone_number): + params = {"mobilePhoneNumber": phone_number, "code": sms_code} + client.post("/changePhoneNumber", params)
+ +
[文档] @classmethod + def verify_mobile_phone_number(cls, sms_code): + client.post("/verifyMobilePhone/" + sms_code, {})
+ +
[文档] @classmethod + def request_login_sms_code(cls, phone_number, validate_token=None): + params = {"mobilePhoneNumber": phone_number} + if validate_token is not None: + params["validate_token"] = validate_token + client.post("/requestLoginSmsCode", params)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt new file mode 100644 index 00000000..3af9bd8f --- /dev/null +++ b/docs/_sources/index.rst.txt @@ -0,0 +1,151 @@ +================================================ +LeanCloud-Python-SDK API 文档 +================================================ + +.. toctree:: + :maxdepth: 4 + +leancloud +========= + +.. autofunction:: leancloud.init + +.. autofunction:: leancloud.use_master_key + +.. autofunction:: leancloud.use_production + +.. autofunction:: leancloud.use_region + +.. autoclass:: leancloud.FriendshipQuery + :show-inheritance: + :members: + :undoc-members: + +.. autoclass:: leancloud.LeanCloudError + :show-inheritance: + :members: + :undoc-members: + +.. autoclass:: leancloud.LeanCloudWarning + :show-inheritance: + :members: + :undoc-members: + +Object +------ + +.. autoclass:: leancloud.Object + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +User +---- + +.. autoclass:: leancloud.User + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +File +---- + +.. autoclass:: leancloud.File + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +Query +----- +.. autoclass:: leancloud.Query + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +Relation +-------- + +.. autoclass:: leancloud.Relation + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +Role +---- + +.. autoclass:: leancloud.Role + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +ACL +--- + +.. autoclass:: leancloud.ACL + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + + +GeoPoint +-------- + +.. autoclass:: leancloud.GeoPoint + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +Engine +------ + +.. autoclass:: leancloud.Engine + :show-inheritance: + :inherited-members: + :members: + :undoc-members: + +HttpsRedirectMiddleware +----------------------- + +.. autoclass:: leancloud.engine.HttpsRedirectMiddleware + :show-inheritance: + :members: + :undoc-members: + +CookieSessionMiddleware +----------------------- + +.. autoclass:: leancloud.engine.CookieSessionMiddleware + :show-inheritance: + :members: + :undoc-members: + +leancloud.push +============== + +.. automodule:: leancloud.push + :members: + :undoc-members: + :show-inheritance: + +leancloud.cloud +=================== + +.. automodule:: leancloud.cloud + :members: + :undoc-members: + :show-inheritance: + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/docs/_static/_sphinx_javascript_frameworks_compat.js b/docs/_static/_sphinx_javascript_frameworks_compat.js new file mode 100644 index 00000000..8549469d --- /dev/null +++ b/docs/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1,134 @@ +/* + * _sphinx_javascript_frameworks_compat.js + * ~~~~~~~~~~ + * + * Compatability shim for jQuery and underscores.js. + * + * WILL BE REMOVED IN Sphinx 6.0 + * xref RemovedInSphinx60Warning + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/docs/_static/basic.css b/docs/_static/basic.css new file mode 100644 index 00000000..7d5974c3 --- /dev/null +++ b/docs/_static/basic.css @@ -0,0 +1,928 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} +a.brackets:before, +span.brackets > a:before{ + content: "["; +} + +a.brackets:after, +span.brackets > a:after { + content: "]"; +} + + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +/* Docutils 0.17 and older (footnotes & citations) */ +dl.footnote > dt, +dl.citation > dt { + float: left; + margin-right: 0.5em; +} + +dl.footnote > dd, +dl.citation > dd { + margin-bottom: 0em; +} + +dl.footnote > dd:after, +dl.citation > dd:after { + content: ""; + clear: both; +} + +/* Docutils 0.18+ (footnotes & citations) */ +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +/* Footnotes & citations ends */ + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dt:after { + content: ":"; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_static/css/badge_only.css b/docs/_static/css/badge_only.css new file mode 100644 index 00000000..e380325b --- /dev/null +++ b/docs/_static/css/badge_only.css @@ -0,0 +1 @@ +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 00000000..6cb60000 Binary files /dev/null and b/docs/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 00000000..7059e231 Binary files /dev/null and b/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 00000000..f815f63f Binary files /dev/null and b/docs/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 00000000..f2c76e5b Binary files /dev/null and b/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.eot b/docs/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 00000000..e9f60ca9 Binary files /dev/null and b/docs/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.svg b/docs/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 00000000..855c845e --- /dev/null +++ b/docs/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/css/fonts/fontawesome-webfont.ttf b/docs/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 00000000..35acda2f Binary files /dev/null and b/docs/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff b/docs/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 00000000..400014a4 Binary files /dev/null and b/docs/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff2 b/docs/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 00000000..4d13fc60 Binary files /dev/null and b/docs/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff b/docs/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 00000000..88ad05b9 Binary files /dev/null and b/docs/_static/css/fonts/lato-bold-italic.woff differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff2 b/docs/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 00000000..c4e3d804 Binary files /dev/null and b/docs/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/docs/_static/css/fonts/lato-bold.woff b/docs/_static/css/fonts/lato-bold.woff new file mode 100644 index 00000000..c6dff51f Binary files /dev/null and b/docs/_static/css/fonts/lato-bold.woff differ diff --git a/docs/_static/css/fonts/lato-bold.woff2 b/docs/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 00000000..bb195043 Binary files /dev/null and b/docs/_static/css/fonts/lato-bold.woff2 differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff b/docs/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 00000000..76114bc0 Binary files /dev/null and b/docs/_static/css/fonts/lato-normal-italic.woff differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff2 b/docs/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 00000000..3404f37e Binary files /dev/null and b/docs/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/docs/_static/css/fonts/lato-normal.woff b/docs/_static/css/fonts/lato-normal.woff new file mode 100644 index 00000000..ae1307ff Binary files /dev/null and b/docs/_static/css/fonts/lato-normal.woff differ diff --git a/docs/_static/css/fonts/lato-normal.woff2 b/docs/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 00000000..3bf98433 Binary files /dev/null and b/docs/_static/css/fonts/lato-normal.woff2 differ diff --git a/docs/_static/css/theme.css b/docs/_static/css/theme.css new file mode 100644 index 00000000..0d9ae7e1 --- /dev/null +++ b/docs/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,.wy-nav-top a,.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.rst-content .wy-breadcrumbs li tt,.wy-breadcrumbs li .rst-content tt,.wy-breadcrumbs li code{padding:5px;border:none;background:none}.rst-content .wy-breadcrumbs li tt.literal,.wy-breadcrumbs li .rst-content tt.literal,.wy-breadcrumbs li code.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.field-list>dt:after,html.writer-html5 .rst-content dl.footnote>dt:after{content:":"}html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.footnote>dt>span.brackets{margin-right:.5rem}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{font-style:italic}html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.footnote>dd p,html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{font-size:inherit;line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js new file mode 100644 index 00000000..c3db08d1 --- /dev/null +++ b/docs/_static/doctools.js @@ -0,0 +1,264 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + parent.insertBefore( + span, + parent.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.highlightSearchWords(); + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * highlight the search words provided in the url in the text + */ + highlightSearchWords: () => { + const highlight = + new URLSearchParams(window.location.search).get("highlight") || ""; + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + const url = new URL(window.location); + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + const blacklistedElements = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", + ]); + document.addEventListener("keydown", (event) => { + if (blacklistedElements.has(document.activeElement.tagName)) return; // bail for input elements + if (event.altKey || event.ctrlKey || event.metaKey) return; // bail with special keys + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + case "Escape": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.hideSearchWords(); + event.preventDefault(); + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js new file mode 100644 index 00000000..a844c873 --- /dev/null +++ b/docs/_static/documentation_options.js @@ -0,0 +1,14 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '2.6.1', + LANGUAGE: 'zh-CN', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: false, +}; \ No newline at end of file diff --git a/docs/_static/file.png b/docs/_static/file.png new file mode 100644 index 00000000..a858a410 Binary files /dev/null and b/docs/_static/file.png differ diff --git a/docs/_static/jquery-3.6.0.js b/docs/_static/jquery-3.6.0.js new file mode 100644 index 00000000..fc6c299b --- /dev/null +++ b/docs/_static/jquery-3.6.0.js @@ -0,0 +1,10881 @@ +/*! + * jQuery JavaScript Library v3.6.0 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2021-03-02T17:08Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 + // Plus for old WebKit, typeof returns "function" for HTML collections + // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) + return typeof obj === "function" && typeof obj.nodeType !== "number" && + typeof obj.item !== "function"; + }; + + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + +var document = window.document; + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var + version = "3.6.0", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return flat( ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.6 + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://js.foundation/ + * + * Date: 2021-02-16 + */ +( function( window ) { +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ( {} ).hasOwnProperty, + arr = [], + pop = arr.pop, + pushNative = arr.push, + push = arr.push, + slice = arr.slice, + + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[ i ] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + + "ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] + // or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rhtml = /HTML$/i, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + return nonHex ? + + // Strip the backslash prefix from a non-hex escape sequence + nonHex : + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + // Support: Android<4.0 + // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + + // Can't trust NodeList.length + while ( ( target[ j++ ] = els[ i++ ] ) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + // ID selector + if ( ( m = match[ 1 ] ) ) { + + // Document context + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && ( elem = newContext.getElementById( m ) ) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && + + // Support: IE 8 only + // Exclude object elements + ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + if ( newContext !== context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split( "|" ), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[ i ] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( ( cur = cur.nextSibling ) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return ( name === "input" || name === "button" ) && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11 + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + /* jshint -W018 */ + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + var namespace = elem && elem.namespaceURI, + docElem = elem && ( elem.ownerDocument || elem ).documentElement; + + // Support: IE <=8 + // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes + // https://bugs.jquery.com/ticket/4833 + return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9 - 11+, Edge 12 - 18+ + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + // Support: IE 11, Edge + if ( subWindow.addEventListener ) { + subWindow.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( subWindow.attachEvent ) { + subWindow.attachEvent( "onunload", unloadHandler ); + } + } + + // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, + // Safari 4 - 5 only, Opera <=11.6 - 12.x only + // IE/Edge & older browsers don't support the :scope pseudo-class. + // Support: Safari 6.0 only + // Safari 6.0 supports :scope but it's an alias of :root there. + support.scope = assert( function( el ) { + docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); + return typeof el.querySelectorAll !== "undefined" && + !el.querySelectorAll( ":scope fieldset div" ).length; + } ); + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert( function( el ) { + el.className = "i"; + return !el.getAttribute( "className" ); + } ); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert( function( el ) { + el.appendChild( document.createComment( "" ) ); + return !el.getElementsByTagName( "*" ).length; + } ); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert( function( el ) { + docElem.appendChild( el ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + } ); + + // ID filter and find + if ( support.getById ) { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find[ "TAG" ] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See https://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert( function( el ) { + + var input; + + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // https://bugs.jquery.com/ticket/12359 + docElem.appendChild( el ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + // Support: Firefox <=3.6 - 5 only + // Old Firefox doesn't throw on a badly-escaped identifier. + el.querySelectorAll( "\\\f" ); + rbuggyQSA.push( "[\\r\\n\\f]" ); + } ); + + assert( function( el ) { + el.innerHTML = "" + + ""; + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( el.querySelectorAll( "[name=d]" ).length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE9-11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + docElem.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: Opera 10 - 11 only + // Opera 10-11 does not throw on post-comma invalid pseudos + el.querySelectorAll( "*,:x" ); + rbuggyQSA.push( ",.*:" ); + } ); + } + + if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector ) ) ) ) { + + assert( function( el ) { + + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( el, "*" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( el, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + } ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); + } : + function( a, b ) { + if ( b ) { + while ( ( b = b.parentNode ) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + // Choose the first element that is related to our preferred document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a == document || a.ownerDocument == preferredDoc && + contains( preferredDoc, a ) ) { + return -1; + } + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b == document || b.ownerDocument == preferredDoc && + contains( preferredDoc, b ) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + return a == document ? -1 : + b == document ? 1 : + /* eslint-enable eqeqeq */ + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( ( cur = cur.parentNode ) ) { + ap.unshift( cur ); + } + cur = b; + while ( ( cur = cur.parentNode ) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[ i ] === bp[ i ] ) { + i++; + } + + return i ? + + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[ i ], bp[ i ] ) : + + // Otherwise nodes in our document sort first + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + ap[ i ] == preferredDoc ? -1 : + bp[ i ] == preferredDoc ? 1 : + /* eslint-enable eqeqeq */ + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( support.matchesSelector && documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; +}; + +Sizzle.escape = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + // If no nodeType, this is expected to be an array + while ( ( node = elem[ i++ ] ) ) { + + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || + match[ 5 ] || "" ).replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + + ")" + className + "(" + whitespace + "|$)" ) ) && classCache( + className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + /* eslint-disable max-len */ + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + /* eslint-enable max-len */ + + }; + }, + + "CHILD": function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + // Use previously-cached element index if available + if ( useCache ) { + + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + + // Use the same loop as above to seek `elem` from the start + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + // Potentially complex pseudos + "not": markFunction( function( selector ) { + + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + // Don't keep the element (issue #299) + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + "has": markFunction( function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + } ), + + "contains": markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; + }; + } ), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + + // lang value must be a valid identifier + if ( !ridentifier.test( lang || "" ) ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && + ( !document.hasFocus || document.hasFocus() ) && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + // Boolean properties + "enabled": createDisabledPseudo( false ), + "disabled": createDisabledPseudo( true ), + + "checked": function( elem ) { + + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return ( nodeName === "input" && !!elem.checked ) || + ( nodeName === "option" && !!elem.selected ); + }, + + "selected": function( elem ) { + + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + // eslint-disable-next-line no-unused-expressions + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos[ "empty" ]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo( function() { + return [ 0 ]; + } ), + + "last": createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + "even": createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "odd": createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "lt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? + argument + length : + argument > length ? + length : + argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "gt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rcombinators.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || + ( outerCache[ elem.uniqueID ] = {} ); + + if ( skip && skip === elem.nodeName.toLowerCase() ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = uniqueCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( + selector || "*", + context.nodeType ? [ context ] : context, + [] + ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + // Restore matcherIn since elem is not yet a final match + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens + .slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ), + + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + outermostContext = context == document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + + // They will have gone through all possible matchers + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( + selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) + ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find[ "ID" ]( token.matches[ 0 ] + .replace( runescape, funescape ), context ) || [] )[ 0 ]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + // Abort if we hit a combinator + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + // Search, expanding context for leading sibling combinators + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || + context + ) ) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert( function( el ) { + + // Should return 1, but returns 4 (following) + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert( function( el ) { + el.innerHTML = ""; + return el.firstChild.getAttribute( "href" ) === "#"; +} ) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + } ); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert( function( el ) { + el.innerHTML = ""; + el.firstChild.setAttribute( "value", "" ); + return el.firstChild.getAttribute( "value" ) === ""; +} ) ) { + addHandle( "value", function( elem, _name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + } ); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert( function( el ) { + return el.getAttribute( "disabled" ) == null; +} ) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; + } + } ); +} + +return Sizzle; + +} )( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; + +// Deprecated +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; +jQuery.escapeSelector = Sizzle.escape; + + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + + + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +} +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + + + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + // Single element + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + // Arraylike of elements (jQuery, arguments, Array) + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + // Shortcut simple #id case for speed + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + // Inject the element directly into the jQuery object + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + // Positional selectors never match, since there's no _selection_ context + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + // Support: IE 11+ + // elements with no `data` attribute has an object + // `contentDocument` with a `null` prototype. + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = locked || options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + // Check for promise aspect first to privilege synchronous behavior + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + // Other thenables + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + // Other non-thenables + } else { + + // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: + // * false: [ value ].slice( 0 ) => resolve( value ) + // * true: [ value ].slice( 1 ) => resolve() + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + // For Promises/A+, convert exceptions into rejections + // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in + // Deferred#then to conditionally suppress rejection. + } catch ( value ) { + + // Support: Android 4.0 only + // Strict mode functions invoked without .call/.apply get global-object context + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, callbacks, + // ... .then handlers, argument index, [final state] + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + // Map tuples (progress, done, fail) to arguments (done, fail, progress) + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + // deferred.progress(function() { bind to newDefer or newDefer.notify }) + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( isFunction( then ) ) { + + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + // Handle all other returned values + } else { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( that, args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.stackTrace ); + } + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + + // Call an optional hook to record the stack, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getStackHook ) { + process.stackTrace = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + // progress_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + // fulfilled_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + // promise.progress = list.add + // promise.done = list.add + // promise.fail = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( + function() { + + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ 3 - i ][ 2 ].disable, + + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + + // progress_callbacks.lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock + ); + } + + // progress_handlers.fire + // fulfilled_handlers.fire + // rejected_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.notify = function() { deferred.notifyWith(...) } + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + // deferred.notifyWith = list.fireWith + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( singleValue ) { + var + + // count of uncompleted subordinates + remaining = arguments.length, + + // count of unprocessed arguments + i = remaining, + + // subordinate fulfillment data + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + // the primary Deferred + primary = jQuery.Deferred(), + + // subordinate callback factory + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + primary.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + // Single- and empty arguments are adopted like Promise.resolve + if ( remaining <= 1 ) { + adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, + !remaining ); + + // Use .then() to unwrap secondary thenables (cf. gh-3000) + if ( primary.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return primary.then(); + } + } + + // Multiple arguments are aggregated like Promise.all array elements + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); + } + + return primary.promise(); + } +} ); + + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, stack ) { + + // Support: IE 8 - 9 only + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); + } +}; + + + + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + + + + +// The deferred used on DOM ready +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + // Wrap jQuery.readyException in a function so that the lookup + // happens at the time of error handling instead of callback + // registration. + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +// The ready event handler and self cleanup method +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +// Support: IE <=9 - 10 only +// Older IE sometimes signals "interactive" too soon +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (#9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + // Always use camelCase key (gh-2257) + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + // Always use camelCase key (gh-2257) + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + // Support array or space separated string of keys + if ( Array.isArray( key ) ) { + + // If key is an array of keys... + // We always set camelCase keys, so remove that. + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <=35 - 45 + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + // Only convert to a number if it doesn't change the string + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE 11 only + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // The key will always be camelCased in Data + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each( function() { + + // We always store the camelCased key + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + + // Otherwise, check computed style + // Support: Firefox <=43 - 45 + // Disconnected elements can have computed display: none, so first confirm that elem is + // in the document. + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); + } + } + } + + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // Support: IE <=9 only + // IE <=9 replaces "; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: IE <=9 only +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE <=9 - 11+ +// focus() and blur() are asynchronous, except when they are no-op. +// So expect focus to be synchronous when the element is already active, +// and blur to be synchronous when the element is not already active. +// (focus and blur are always synchronous in other supported browsers, +// this just defines when we can count on it). +function expectSync( elem, type ) { + return ( elem === safeActiveElement() ) === ( type === "focus" ); +} + +// Support: IE <=9 only +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Only attach events to objects that accept data + if ( !acceptData( elem ) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Ensure that invalid selectors throw exceptions at attach time + // Evaluate against documentElement in case elem is a non-element node (e.g., document) + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + if ( delegateCount && + + // Support: IE <=9 + // Black-hole SVG instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", returnTrue ); + } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; + }, + + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, expectSync ) { + + // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add + if ( !expectSync ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var notAsync, result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + // Saved data should be false in such cases, but might be a leftover capture object + // from an async native handler (gh-4350) + if ( !saved.length ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + // Support: IE <=9 - 11+ + // focus() and blur() are asynchronous + notAsync = expectSync( this, type ); + this[ type ](); + result = dataPriv.get( this, type ); + if ( saved !== result || notAsync ) { + dataPriv.set( this, type, false ); + } else { + result = {}; + } + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + + // Support: Chrome 86+ + // In Chrome, if an element having a focusout handler is blurred by + // clicking outside of it, it invokes the handler synchronously. If + // that handler calls `.remove()` on the element, the data is cleared, + // leaving `result` undefined. We need to guard against this. + return result && result.value; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering the + // native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved.length ) { + + // ...and capture the result + dataPriv.set( this, type, { + value: jQuery.event.trigger( + + // Support: IE <=9 - 11+ + // Extend with the prototype to reset the above stopImmediatePropagation() + jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), + saved.slice( 1 ), + this + ) + } ); + + // Abort handling of the native event + event.stopImmediatePropagation(); + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (#504, #13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || Date.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + which: true +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, expectSync ); + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + // Suppress native focus or blur as it's already being fired + // in leverageNative. + _default: function() { + return true; + }, + + delegateType: delegateType + }; +} ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + // Support: IE <=10 - 11, Edge 12 - 13 only + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Prefer a tbody over its parent table for containing new rows +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (#15098, #14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + + + +( function() { + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computeStyleTests() { + + // This is a singleton, we need to execute it only once + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 + // Some styles come back with percentage values, even though they shouldn't + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + // Nullify the div so it wouldn't be stored in the memory and + // it will also be a sign that checks already performed + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + // Finish early in limited (non-browser) environments + if ( !div.style ) { + return; + } + + // Support: IE <=9 - 11 only + // Style of cloned element affects source element cloned (#8908) + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + // Support: IE 9 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Behavior in IE 9 is more subtle than in newer versions & it passes + // some versions of this test; make sure not to make it pass there! + // + // Support: Firefox 70+ + // Only Firefox includes border widths + // in computed dimensions. (gh-4529) + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; + tr.style.cssText = "border:1px solid"; + + // Support: Chrome 86+ + // Height set through cssText does not get applied. + // Computed height then comes back as 0. + tr.style.height = "1px"; + trChild.style.height = "9px"; + + // Support: Android 8 Chrome 86+ + // In our bodyBackground.html iframe, + // display for all div elements is set to "inline", + // which causes a problem only in Android 8 Chrome 86. + // Ensuring the div is display: block + // gets around this issue. + trChild.style.display = "block"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + + parseInt( trStyle.borderTopWidth, 10 ) + + parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, #12537) + // .css('--customProperty) (#3144) + if ( computed ) { + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +// Return a vendor-prefixed property or undefined +function vendorPropName( name ) { + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0; + + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin + if ( box === "margin" ) { + delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { + + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" + } else { + + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + // Start with computed style + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + + // Support: IE 9 - 11 only + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + if ( ( !support.boxSizingReliable() && isBorderBox || + + // Support: IE 10 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Interestingly, in some cases IE 9 doesn't suffer from this issue. + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || + + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + // Make sure the element is visible & connected + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + // Normalize "" and auto + val = parseFloat( val ) || 0; + + // Adjust for the element's box model + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val + ) + ) + "px"; +} + +jQuery.extend( { + + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "animationIterationCount": true, + "columnCount": true, + "fillOpacity": true, + "flexGrow": true, + "flexShrink": true, + "fontWeight": true, + "gridArea": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnStart": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowStart": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: {}, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (#7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug #9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (#7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + // Use a property on the element directly when it is not a DOM element, + // or when there is no matching style property that exists. + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + // Passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails. + // Simple values such as "10px" are parsed to Float; + // complex values such as "rotate(1rad)" are returned as-is. + result = jQuery.css( tween.elem, tween.prop, "" ); + + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + // Use step hook for back compat. + // Use cssHook if its there. + // Use .style if available and use plain properties where available. + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Support: IE <=9 only +// Panic based approach to setting things on disconnected nodes +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +// Back compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +// Animations created synchronously will run synchronously +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // If we include width, step value is 1 to do all cssExpand values, + // otherwise step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + // We're done with this property + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + // Queue-skipping animations hijack the fx hooks + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + // Ensure the complete handler is called before this completes + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + // Detect show/hide animations + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // Pretend to be hidden if this is a "show" and + // there is still data from a stopped show/hide + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + // Ignore all other no-op show/hide data + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + // Bail out if this is a no-op like .hide().hide() + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + // Restrict "overflow" and "display" styles during box animations + if ( isBox && elem.nodeType === 1 ) { + + // Support: IE <=9 - 11, Edge 12 - 15 + // Record all 3 overflow attributes because IE does not infer the shorthand + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Identify a display type, preferring old show/hide data over the CSS cascade + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + // Get nonempty value(s) by temporarily forcing visibility + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + // Animate inline elements as inline-block + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + // Restore the original display value at the end of pure show/hide animations + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + // Implement show/hide animations + propTween = false; + for ( prop in orig ) { + + // General show/hide setup for this element animation + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + // Store hidden/visible for toggle so `.stop().toggle()` "reverses" + if ( toggle ) { + dataShow.hidden = !hidden; + } + + // Show elements before animating them + if ( hidden ) { + showHide( [ elem ], true ); + } + + /* eslint-disable no-loop-func */ + + anim.done( function() { + + /* eslint-enable no-loop-func */ + + // The final step of a "hide" animation is actually hiding the element + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + // Per-property setup + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // Not quite $.extend, this won't overwrite existing keys. + // Reusing 'index' because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + // Don't match elem in the :animated selector + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + // Support: Android 2.3 only + // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + // If there's more to do, yield + if ( percent < 1 && length ) { + return remaining; + } + + // If this was an empty animation, synthesize a final progress notification + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + // Resolve the animation and report its conclusion + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + // If we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // Resolve when we played the last frame; otherwise, reject + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + // Attach callbacks from options + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + // Go to the end state if fx are off + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + // Normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + // Show any hidden elements after setting opacity to 0 + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + // Animate to the value specified + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // Start the next in the queue if the last step wasn't forced. + // Timers currently will call their complete callbacks, which + // will dequeue but only if they were gotoEnd. + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // Enable finishing flag on private data + data.finish = true; + + // Empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // Look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // Look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // Turn off finishing flag + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +// Generate shortcuts for custom animations +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + // Run the timer and safely remove it when done (allowing for external removal) + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) > -1 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + return this.each( function() { + var className, i, self, classNames; + + if ( isValidValue ) { + + // Toggle individual class names + i = 0; + self = jQuery( this ); + classNames = classesToArray( value ); + + while ( ( className = classNames[ i++ ] ) ) { + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (#14686, #14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion + + +support.focusin = "onfocusin" in window; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +// Support: Firefox <=44 +// Firefox doesn't have focus(in | out) events +// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 +// +// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 +// focus(in | out) events fire after focus & blur events, +// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order +// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 +if ( !support.focusin ) { + jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + dataPriv.remove( doc, fix ); + + } else { + dataPriv.access( doc, fix, attaches ); + } + } + }; + } ); +} +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml, parserErrorElem; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) {} + + parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; + if ( !xml || parserErrorElem ) { + jQuery.error( "Invalid XML: " + ( + parserErrorElem ? + jQuery.map( parserErrorElem.childNodes, function( el ) { + return el.textContent; + } ).join( "\n" ) : + data + ) ); + } + return xml; +}; + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ).filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ).map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = "*/".concat( "*" ), + + // Anchor tag for parsing the document origin + originAnchor = document.createElement( "a" ); + +originAnchor.href = location.href; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + // For each dataType in the dataTypeExpression + while ( ( dataType = dataTypes[ i++ ] ) ) { + + // Prepend if requested + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + // Otherwise append + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; + + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": JSON.parse, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var transport, + + // URL without anti-cache param + cacheURL, + + // Response headers + responseHeadersString, + responseHeaders, + + // timeout handle + timeoutTimer, + + // Url cleanup var + urlAnchor, + + // Request state (becomes false upon send and true upon completion) + completed, + + // To know if global events are to be dispatched + fireGlobals, + + // Loop variable + i, + + // uncached part of the url + uncached, + + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + + // Callbacks context + callbackContext = s.context || s, + + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + // Status-dependent callbacks + statusCode = s.statusCode || {}, + + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + + // Default abort message + strAbort = "canceled", + + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + // Raw string + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } else { + + // Lazy-add the new callbacks in a way that preserves old ones + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ); + + // Add protocol if not provided (prefilters might expect it) + // Handle falsy url in the settings object (#10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + // Alias method option to type as per ticket #12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + // A cross-domain request is in order when the origin doesn't match the current origin. + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + // Support: IE <=8 - 11, Edge 12 - 15 + // IE throws exception on accessing the href property if url is malformed, + // e.g. http://example.com:80x/ + try { + urlAnchor.href = s.url; + + // Support: IE <=8 - 11 only + // Anchor's host property isn't correctly set when s.url is relative + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + // If there is an error parsing the URL, assume it is crossDomain, + // it can be rejected by the transport if it is invalid + s.crossDomain = true; + } + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( completed ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) + fireGlobals = jQuery.event && s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + // Remove hash to simplify url manipulation + cacheURL = s.url.replace( rhash, "" ); + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // Remember the hash so we can put it back + uncached = s.url.slice( cacheURL.length ); + + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add or update anti-cache param if needed + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + // Put hash and anti-cache on the URL that will be requested (gh-1732) + s.url = cacheURL + uncached; + + // Change '%20' to '+' if this is encoded form body content (gh-2658) + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + // Abort if not done already and return + return jqXHR.abort(); + } + + // Aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + // If request was aborted inside ajaxSend, stop there + if ( completed ) { + return jqXHR; + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + // Rethrow post-completion exceptions + if ( completed ) { + throw e; + } + + // Propagate others as results + done( -1, e ); + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Ignore repeat invocations + if ( completed ) { + return; + } + + completed = true; + + // Clear timeout if it exists + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // Use a noop converter for missing script but not if jsonp + if ( !isSuccess && + jQuery.inArray( "script", s.dataTypes ) > -1 && + jQuery.inArray( "json", s.dataTypes ) < 0 ) { + s.converters[ "text script" ] = function() {}; + } + + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + // If successful, handle type chaining + if ( isSuccess ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + // Extract error from statusText and normalize for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + // Shift arguments if data argument was omitted + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + // The url can be an options object (which then must have .url) + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + // Make this explicit, since user can override this through ajaxSetup (#11264) + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + // File protocol always yields status code 0, assume 200 + 0: 200, + + // Support: IE <=9 only + // #1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + // Support: IE <=9 only + // On a manual native abort, IE9 throws + // errors on any property access that is not readyState + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + // File: protocol always yields status 0; see #8605, #14207 + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + // Support: IE <=9 only + // IE9 has no XHR2 but throws on binary (trac-11426) + // For XHR2 non-text, let the caller handle it (gh-2498) + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; + }; + + // Listen to events + xhr.onload = callback(); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + + // Support: IE 9 only + // Use onreadystatechange to replace onabort + // to handle uncaught aborts + if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; + } else { + xhr.onreadystatechange = function() { + + // Check readyState before timeout as it changes + if ( xhr.readyState === 4 ) { + + // Allow onerror to be called first, + // but that will not handle a native abort + // Also, save errorCallback to a variable + // as xhr.onerror cannot be accessed + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; + } + + // Create the abort callback + callback = callback( "abort" ); + + try { + + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + + // #14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; + } + } + }, + + abort: function() { + if ( callback ) { + callback(); + } + } + }; + } +} ); + + + + +// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) +jQuery.ajaxPrefilter( function( s ) { + if ( s.crossDomain ) { + s.contents.script = false; + } +} ); + +// Install script dataType +jQuery.ajaxSetup( { + accepts: { + script: "text/javascript, application/javascript, " + + "application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /\b(?:java|ecma)script\b/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +} ); + +// Handle cache's special case and crossDomain +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + } +} ); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function( s ) { + + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { + var script, callback; + return { + send: function( _, complete ) { + script = jQuery( " + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • 索引
  • +
  • +
  • +
+
+
+
+
+ + +

索引

+ +
+ A + | B + | C + | D + | E + | F + | G + | H + | I + | K + | L + | M + | N + | O + | P + | Q + | R + | S + | U + | V + | W + | + +
+

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

H

+ + + +
+ +

I

+ + + +
+ +

K

+ + +
+ +

L

+ + + +
+ +

M

+ + + +
+ +

N

+ + + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

Q

+ + + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ +

W

+ + + +
+ +

+ + +
+ + + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..640e5e08 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,2878 @@ + + + + + + + LeanCloud-Python-SDK API 文档 — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

LeanCloud-Python-SDK API 文档

+
+
+
+

leancloud

+
+
+leancloud.init(app_id, app_key=None, master_key=None, hook_key=None)[源代码]
+

初始化 LeanCloud 的 AppId / AppKey / MasterKey

+
+
参数
+
    +
  • app_id (string_types) – 应用的 Application ID

  • +
  • app_key (None or string_types) – 应用的 Application Key

  • +
  • master_key (None or string_types) – 应用的 Master Key

  • +
  • hook_key (None or string_type) – application’s hook key

  • +
+
+
+
+ +
+
+leancloud.use_master_key(flag=True)[源代码]
+

是否使用 master key 发送请求。 +如果不调用此函数,会根据 leancloud.init 的参数来决定是否使用 master key。

+
+
+
+ +
+
+leancloud.use_production(flag)[源代码]
+

调用生产环境 / 开发环境的 cloud func / cloud hook +默认调用生产环境。

+
+ +
+
+leancloud.use_region(region)[源代码]
+
+ +
+
+class leancloud.FriendshipQuery(query_class)[源代码]
+

基类:Query

+
+ +
+
+class leancloud.LeanCloudError(code, error)[源代码]
+

基类:Exception

+
+ +
+
+class leancloud.LeanCloudWarning[源代码]
+

基类:UserWarning

+
+ +
+

Object

+
+
+class leancloud.Object(**attrs)[源代码]
+

基类:object

+
+
+add(attr, item)[源代码]
+

在对象此字段对应的数组末尾添加指定对象。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • item – 要添加的对象

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+add_unique(attr, item)[源代码]
+

在对象此字段对应的数组末尾添加指定对象,如果此对象并没有包含在字段中。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • item – 要添加的对象

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+static as_class(arg)[源代码]
+
+ +
+
+bit_and(attr, value)[源代码]
+
+ +
+
+bit_or(attr, value)[源代码]
+
+ +
+
+bit_xor(attr, value)[源代码]
+
+ +
+
+clear()[源代码]
+

将当前对象所有字段全部移除。

+
+
返回
+

当前对象

+
+
+
+ +
+
+classmethod create(class_name, **attributes)[源代码]
+

根据参数创建一个 leancloud.Object 的子类的实例化对象

+
+
参数
+
    +
  • class_name (string_types) – 子类名称

  • +
  • attributes – 对象属性

  • +
+
+
返回
+

派生子类的实例

+
+
返回类型
+

Object

+
+
+
+ +
+
+classmethod create_without_data(id_)[源代码]
+

根据 objectId 创建一个 leancloud.Object,代表一个服务器上已经存在的对象。可以调用 fetch 方法来获取服务器上的数据

+
+
参数
+

id (string_types) – 对象的 objectId

+
+
返回
+

没有数据的对象

+
+
返回类型
+

Object

+
+
+
+ +
+
+destroy()[源代码]
+

从服务器上删除这个对象

+
+
返回类型
+

None

+
+
+
+ +
+
+classmethod destroy_all(objs)[源代码]
+

在一个请求中 destroy 多个 leancloud.Object 对象实例。

+
+
参数
+

objs (list) – 需要 destroy 的对象

+
+
+
+ +
+
+disable_after_hook()[源代码]
+
+ +
+
+disable_before_hook()[源代码]
+
+ +
+
+dump()[源代码]
+
+ +
+
+classmethod extend(name)[源代码]
+

派生一个新的 leancloud.Object 子类

+
+
参数
+

name (string_types) – 子类名称

+
+
返回
+

派生的子类

+
+
返回类型
+

ObjectMeta

+
+
+
+ +
+
+fetch(select=None, include=None)[源代码]
+

从服务器获取当前对象所有的值,如果与本地值不同,将会覆盖本地的值。

+
+
返回
+

当前对象

+
+
+
+ +
+
+get(attr, default=None, deafult=None)[源代码]
+

获取对象字段的值

+
+
参数
+

attr (string_types) – 字段名

+
+
返回
+

字段值

+
+
+
+ +
+
+get_acl()[源代码]
+

返回当前对象的 ACL。

+
+
返回
+

当前对象的 ACL

+
+
返回类型
+

leancloud.ACL

+
+
+
+ +
+
+has(attr)[源代码]
+

判断此字段是否有值

+
+
参数
+

attr – 字段名

+
+
返回
+

当有值时返回 True, 否则返回 False

+
+
返回类型
+

bool

+
+
+
+ +
+
+ignore_hook(hook_name)[源代码]
+
+ +
+
+increment(attr, amount=1)[源代码]
+

在对象此字段上自增对应的数值,如果数值没有指定,默认为一。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • amount – 自增量

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+is_dirty(attr=None)[源代码]
+
+ +
+
+is_existed()[源代码]
+

判断当前对象是否在服务器上已经存在。

+
+
返回类型
+

bool

+
+
+
+ +
+
+is_new()[源代码]
+

判断当前对象是否已经保存至服务器。

+

该方法为 SDK 内部使用(save 调用此方法 dispatch 保存操作为 REST API 的 POST 和 PUT 请求)。 +查询对象是否在服务器上存在请使用 is_existed 方法。

+
+
返回类型
+

bool

+
+
+
+ +
+
+relation(attr)[源代码]
+

返回对象上相应字段的 Relation

+
+
参数
+

attr (string_types) – 字段名

+
+
返回
+

Relation

+
+
返回类型
+

leancloud.Relation

+
+
+
+ +
+
+remove(attr, item)[源代码]
+

在对象此字段对应的数组中,将指定对象全部移除。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • item – 要移除的对象

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+save(where=None, fetch_when_save=None)[源代码]
+

将对象数据保存至服务器

+
+
返回
+

None

+
+
返回类型
+

None

+
+
+
+ +
+
+classmethod save_all(objs)[源代码]
+

在一个请求中 save 多个 leancloud.Object 对象实例。

+
+
参数
+

objs (list) – 需要 save 的对象

+
+
+
+ +
+
+set(key_or_attrs, value=None, unset=False)[源代码]
+

在当前对象此字段上赋值

+
+
参数
+
    +
  • key_or_attrs (string_types or dict) – 字段名,或者一个包含 字段名 / 值的 dict

  • +
  • value – 字段值

  • +
  • unset

  • +
+
+
返回
+

当前对象,供链式调用

+
+
+
+ +
+
+set_acl(acl)[源代码]
+

为当前对象设置 ACL

+
+
返回
+

当前对象

+
+
+
+ +
+
+unset(attr)[源代码]
+

在对象上移除此字段。

+
+
参数
+

attr – 字段名

+
+
返回
+

当前对象

+
+
+
+ +
+
+validate(attrs)[源代码]
+
+ +
+ +
+
+

User

+
+
+class leancloud.User(**attrs)[源代码]
+

基类:Object

+
+
+add(attr, item)
+

在对象此字段对应的数组末尾添加指定对象。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • item – 要添加的对象

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+add_unique(attr, item)
+

在对象此字段对应的数组末尾添加指定对象,如果此对象并没有包含在字段中。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • item – 要添加的对象

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+static as_class(arg)
+
+ +
+
+classmethod become(session_token)[源代码]
+

通过 session token 获取用户对象

+
+
参数
+

session_token – 用户的 session token

+
+
返回
+

leancloud.User

+
+
+
+ +
+
+bit_and(attr, value)
+
+ +
+
+bit_or(attr, value)
+
+ +
+
+bit_xor(attr, value)
+
+ +
+
+classmethod change_phone_number(sms_code, phone_number)[源代码]
+
+ +
+
+clear()
+

将当前对象所有字段全部移除。

+
+
返回
+

当前对象

+
+
+
+ +
+
+classmethod create(class_name, **attributes)
+

根据参数创建一个 leancloud.Object 的子类的实例化对象

+
+
参数
+
    +
  • class_name (string_types) – 子类名称

  • +
  • attributes – 对象属性

  • +
+
+
返回
+

派生子类的实例

+
+
返回类型
+

Object

+
+
+
+ +
+
+classmethod create_followee_query(user_id)[源代码]
+
+ +
+
+classmethod create_follower_query(user_id)[源代码]
+
+ +
+
+classmethod create_without_data(id_)
+

根据 objectId 创建一个 leancloud.Object,代表一个服务器上已经存在的对象。可以调用 fetch 方法来获取服务器上的数据

+
+
参数
+

id (string_types) – 对象的 objectId

+
+
返回
+

没有数据的对象

+
+
返回类型
+

Object

+
+
+
+ +
+
+destroy()
+

从服务器上删除这个对象

+
+
返回类型
+

None

+
+
+
+ +
+
+classmethod destroy_all(objs)
+

在一个请求中 destroy 多个 leancloud.Object 对象实例。

+
+
参数
+

objs (list) – 需要 destroy 的对象

+
+
+
+ +
+
+disable_after_hook()
+
+ +
+
+disable_before_hook()
+
+ +
+
+dump()
+
+ +
+
+classmethod extend(name)
+

派生一个新的 leancloud.Object 子类

+
+
参数
+

name (string_types) – 子类名称

+
+
返回
+

派生的子类

+
+
返回类型
+

ObjectMeta

+
+
+
+ +
+
+fetch(select=None, include=None)
+

从服务器获取当前对象所有的值,如果与本地值不同,将会覆盖本地的值。

+
+
返回
+

当前对象

+
+
+
+ +
+
+follow(target_id)[源代码]
+

关注一个用户。

+
+
参数
+

target_id – 需要关注的用户的 id

+
+
+
+ +
+
+get(attr, default=None, deafult=None)
+

获取对象字段的值

+
+
参数
+

attr (string_types) – 字段名

+
+
返回
+

字段值

+
+
+
+ +
+
+get_acl()
+

返回当前对象的 ACL。

+
+
返回
+

当前对象的 ACL

+
+
返回类型
+

leancloud.ACL

+
+
+
+ +
+
+classmethod get_current() Optional[User][源代码]
+
+ +
+
+get_email()[源代码]
+
+ +
+
+get_mobile_phone_number()[源代码]
+
+ +
+
+get_roles()[源代码]
+
+ +
+
+get_session_token()[源代码]
+
+ +
+
+get_username()[源代码]
+
+ +
+
+has(attr)
+

判断此字段是否有值

+
+
参数
+

attr – 字段名

+
+
返回
+

当有值时返回 True, 否则返回 False

+
+
返回类型
+

bool

+
+
+
+ +
+
+ignore_hook(hook_name)
+
+ +
+
+increment(attr, amount=1)
+

在对象此字段上自增对应的数值,如果数值没有指定,默认为一。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • amount – 自增量

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+is_authenticated()[源代码]
+

判断当前用户对象是否已登录。 +会先检查此用户对象上是否有 session_token,如果有的话,会继续请求服务器验证 session_token 是否合法。

+
+ +
+
+property is_current
+
+ +
+
+is_dirty(attr=None)
+
+ +
+
+is_existed()
+

判断当前对象是否在服务器上已经存在。

+
+
返回类型
+

bool

+
+
+
+ +
+
+is_linked(provider)[源代码]
+
+ +
+
+is_new()
+

判断当前对象是否已经保存至服务器。

+

该方法为 SDK 内部使用(save 调用此方法 dispatch 保存操作为 REST API 的 POST 和 PUT 请求)。 +查询对象是否在服务器上存在请使用 is_existed 方法。

+
+
返回类型
+

bool

+
+
+
+ +
+ +
+ +
+
+login(username=None, password=None, email=None)[源代码]
+

登录用户。成功登录后,服务器会返回用户的 sessionToken 。

+
+
参数
+
    +
  • username – 用户名

  • +
  • email – 邮箱地址(username 和 email 这两个参数必须传入一个且仅能传入一个)

  • +
  • password – 用户密码

  • +
+
+
+
+ +
+
+classmethod login_with(platform, third_party_auth_data)[源代码]
+

把第三方平台号绑定到 User 上

+

:param platform: 第三方平台名称 base string

+
+ +
+
+classmethod login_with_mobile_phone(phone_number, password)[源代码]
+
+ +
+
+logout()[源代码]
+
+ +
+
+refresh_session_token()[源代码]
+

重置当前用户 session token。 +会使其他客户端已登录用户登录失效。

+
+ +
+
+relation(attr)
+

返回对象上相应字段的 Relation

+
+
参数
+

attr (string_types) – 字段名

+
+
返回
+

Relation

+
+
返回类型
+

leancloud.Relation

+
+
+
+ +
+
+remove(attr, item)
+

在对象此字段对应的数组中,将指定对象全部移除。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • item – 要移除的对象

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+classmethod request_change_phone_number(phone_number, ttl=None, validate_token=None)[源代码]
+
+ +
+
+classmethod request_email_verify(email)[源代码]
+
+ +
+
+classmethod request_login_sms_code(phone_number, validate_token=None)[源代码]
+
+ +
+
+classmethod request_mobile_phone_verify(phone_number, validate_token=None)[源代码]
+
+ +
+
+classmethod request_password_reset(email)[源代码]
+
+ +
+
+classmethod request_password_reset_by_sms_code(phone_number, validate_token=None)[源代码]
+
+ +
+
+classmethod reset_password_by_sms_code(sms_code, new_password)[源代码]
+
+ +
+
+save(make_current=False)[源代码]
+

将对象数据保存至服务器

+
+
返回
+

None

+
+
返回类型
+

None

+
+
+
+ +
+
+classmethod save_all(objs)
+

在一个请求中 save 多个 leancloud.Object 对象实例。

+
+
参数
+

objs (list) – 需要 save 的对象

+
+
+
+ +
+
+property session_token
+
+ +
+
+set(key_or_attrs, value=None, unset=False)
+

在当前对象此字段上赋值

+
+
参数
+
    +
  • key_or_attrs (string_types or dict) – 字段名,或者一个包含 字段名 / 值的 dict

  • +
  • value – 字段值

  • +
  • unset

  • +
+
+
返回
+

当前对象,供链式调用

+
+
+
+ +
+
+set_acl(acl)
+

为当前对象设置 ACL

+
+
返回
+

当前对象

+
+
+
+ +
+
+classmethod set_current(user)[源代码]
+
+ +
+
+set_email(email)[源代码]
+
+ +
+
+set_mobile_phone_number(phone_number)[源代码]
+
+ +
+
+set_password(password)[源代码]
+
+ +
+
+set_username(username)[源代码]
+
+ +
+
+sign_up(username=None, password=None)[源代码]
+

创建一个新用户。新创建的 User 对象,应该使用此方法来将数据保存至服务器,而不是使用 save 方法。 +用户对象上必须包含 username 和 password 两个字段

+
+ +
+
+classmethod signup_or_login_with_mobile_phone(phone_number, sms_code)[源代码]
+

param phone_nubmer: string_types +param sms_code: string_types

+

在调用此方法前请先使用 request_sms_code 请求 sms code

+
+ +
+
+unfollow(target_id)[源代码]
+

取消关注一个用户。

+
+
参数
+

target_id – 需要关注的用户的 id

+
+
返回
+

+
+
+
+ +
+ +

解绑特定第三方平台

+
+ +
+
+unset(attr)
+

在对象上移除此字段。

+
+
参数
+

attr – 字段名

+
+
返回
+

当前对象

+
+
+
+ +
+
+update_password(old_password, new_password)[源代码]
+
+ +
+
+validate(attrs)
+
+ +
+
+classmethod verify_mobile_phone_number(sms_code)[源代码]
+
+ +
+ +
+
+

File

+
+
+class leancloud.File(name='', data=None, mime_type=None)[源代码]
+

基类:object

+
+
+classmethod create_with_url(name, url, meta_data=None, mime_type=None)[源代码]
+
+ +
+
+classmethod create_without_data(object_id)[源代码]
+
+ +
+
+destroy()[源代码]
+
+ +
+
+fetch()[源代码]
+
+ +
+
+get_acl()[源代码]
+
+ +
+
+get_thumbnail_url(width, height, quality=100, scale_to_fit=True, fmt='png')[源代码]
+
+ +
+
+property metadata
+
+ +
+
+property mime_type
+
+ +
+
+property name
+
+ +
+
+property owner_id
+
+ +
+
+query = <leancloud.query.Query object>
+
+ +
+
+save()[源代码]
+
+ +
+
+set_acl(acl)[源代码]
+
+ +
+
+property set_mime_type
+
+ +
+
+property size
+
+ +
+
+property url
+
+ +
+ +
+
+

Query

+
+
+class leancloud.Query(query_class)[源代码]
+

基类:object

+
+
+add_ascending(key)[源代码]
+

增加查询排序条件。之前指定的排序条件优先级更高。

+
+
参数
+

key – 排序字段名

+
+
返回类型
+

Query

+
+
+
+ +
+
+add_descending(key)[源代码]
+

增加查询排序条件。之前指定的排序条件优先级更高。

+
+
参数
+

key – 排序字段名

+
+
返回类型
+

Query

+
+
+
+ +
+
+classmethod and_(*queries)[源代码]
+

根据传入的 Query 对象,构造一个新的 AND 查询。

+
+
参数
+

queries – 需要构造的子查询列表

+
+
返回类型
+

Query

+
+
+
+ +
+
+ascending(key)[源代码]
+

限制查询返回结果以指定字段升序排序。

+
+
参数
+

key – 排序字段名

+
+
返回类型
+

Query

+
+
+
+ +
+
+contained_in(key, values)[源代码]
+

增加查询条件,限制查询结果指定字段的值在查询值列表中

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • values (list or tuple) – 查询条件值

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+contains(key, value)[源代码]
+

增加查询条件,限制查询结果对象指定最短的值,包含指定字符串。在数据量比较大的情况下会比较慢。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • value – 需要包含的字符串

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+contains_all(key, values)[源代码]
+

增加查询条件,限制查询结果指定字段的值全部包含与查询值列表中

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • values (list or tuple) – 查询条件值

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+count()[源代码]
+

返回满足查询条件的对象的数量。

+
+
返回类型
+

int

+
+
+
+ +
+
+descending(key)[源代码]
+

限制查询返回结果以指定字段降序排序。

+
+
参数
+

key – 排序字段名

+
+
返回类型
+

Query

+
+
+
+ +
+
+classmethod do_cloud_query(cql, *pvalues)[源代码]
+

使用 CQL 来构造查询。CQL 语法参考 这里

+
+
参数
+
    +
  • cql – CQL 语句

  • +
  • pvalues – 查询参数

  • +
+
+
返回类型
+

CQLResult

+
+
+
+ +
+
+does_not_exist(key)[源代码]
+

增加查询条件,限制查询结果对象不包含指定字段

+
+
参数
+

key – 查询条件字段名

+
+
返回类型
+

Query

+
+
+
+ +
+
+does_not_match_key_in_query(key, query_key, query)[源代码]
+

增加查询条件,限制查询结果对象指定字段的值,与另外一个查询对象的返回结果指定的值不相同。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • query_key – 查询对象返回结果的字段名

  • +
  • query (Query) – 查询对象

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+does_not_match_query(key, query)[源代码]
+

增加查询条件,限制查询结果对象指定字段的值,与另外一个查询对象的返回结果不相同。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • query (Query) – 查询对象

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+dump()[源代码]
+
+
返回
+

当前对象的序列化结果

+
+
返回类型
+

dict

+
+
+
+ +
+
+endswith(key, value)[源代码]
+

增加查询条件,限制查询结果对象指定最短的值,以指定字符串结尾。在数据量比较大的情况下会比较慢。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • value – 需要查询的字符串

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+equal_to(key, value)[源代码]
+

增加查询条件,查询字段的值必须为指定值。

+
+
参数
+
    +
  • key – 查询条件的字段名

  • +
  • value – 查询条件的值

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+exists(key)[源代码]
+

增加查询条件,限制查询结果对象包含指定字段

+
+
参数
+

key – 查询条件字段名

+
+
返回类型
+

Query

+
+
+
+ +
+
+find()[源代码]
+

根据查询条件,获取包含所有满足条件的对象。

+
+
返回类型
+

list

+
+
+
+ +
+
+first()[源代码]
+

根据查询获取最多一个对象。

+
+
返回
+

查询结果

+
+
返回类型
+

Object

+
+
Raise
+

LeanCloudError

+
+
+
+ +
+
+get(object_id)[源代码]
+

根据 objectId 查询。

+
+
参数
+

object_id – 要查询对象的 objectId

+
+
返回
+

查询结果

+
+
返回类型
+

Object

+
+
+
+ +
+
+greater_than(key, value)[源代码]
+

增加查询条件,限制查询结果指定字段的值大于查询值

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • value – 查询条件值

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+greater_than_or_equal_to(key, value)[源代码]
+

增加查询条件,限制查询结果指定字段的值大于等于查询值

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • value – 查询条件值名

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+include(*keys)[源代码]
+

指定查询返回结果中包含关联表字段。

+
+
参数
+

keys – 关联子表字段名

+
+
返回类型
+

Query

+
+
+
+ +
+
+include_acl(value=True)[源代码]
+

设置查询结果的对象,是否包含 ACL 字段。需要在控制台选项中开启对应选项才能生效。

+
+
参数
+

value (bool) – 是否包含 ACL,默认为 True

+
+
返回类型
+

Query

+
+
+
+ +
+
+less_than(key, value)[源代码]
+

增加查询条件,限制查询结果指定字段的值小于查询值

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • value – 查询条件值

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+less_than_or_equal_to(key, value)[源代码]
+

增加查询条件,限制查询结果指定字段的值小于等于查询值

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • value – 查询条件值

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+limit(n)[源代码]
+

设置查询返回结果的数量。如果不设置,默认为 100。最大返回数量为 1000,如果超过这个数量,需要使用多次查询来获取结果。

+
+
参数
+

n – 限制结果的数量

+
+
返回类型
+

Query

+
+
+
+ +
+
+matched(key, regex, ignore_case=False, multi_line=False)[源代码]
+

增加查询条件,限制查询结果对象指定字段满足指定的正则表达式。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • regex – 查询正则表达式

  • +
  • ignore_case – 查询是否忽略大小写,默认不忽略

  • +
  • multi_line – 查询是否匹配多行,默认不匹配

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+matches_key_in_query(key, query_key, query)[源代码]
+

增加查询条件,限制查询结果对象指定字段的值,与另外一个查询对象的返回结果指定的值相同。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • query_key – 查询对象返回结果的字段名

  • +
  • query (Query) – 查询对象

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+matches_query(key, query)[源代码]
+

增加查询条件,限制查询结果对象指定字段的值,与另外一个查询对象的返回结果相同。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • query (Query) – 查询对象

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+near(key, point)[源代码]
+

增加查询条件,限制返回结果指定字段值的位置与给定地理位置临近。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • point – 需要查询的地理位置

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+not_contained_in(key, values)[源代码]
+

增加查询条件,限制查询结果指定字段的值不在查询值列表中

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • values (list or tuple) – 查询条件值

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+not_equal_to(key, value)[源代码]
+

增加查询条件,限制查询结果指定字段的值与查询值不同

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • value – 查询条件值

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+classmethod or_(*queries)[源代码]
+

根据传入的 Query 对象,构造一个新的 OR 查询。

+
+
参数
+

queries – 需要构造的子查询列表

+
+
返回类型
+

Query

+
+
+
+ +
+
+scan(batch_size=None, scan_key=None)[源代码]
+
+ +
+
+select(*keys)[源代码]
+

指定查询返回结果中只包含某些字段。可以重复调用,每次调用的包含内容都将会被返回。

+
+
参数
+

keys – 包含字段名

+
+
返回类型
+

Query

+
+
+
+ +
+
+size_equal_to(key, size)[源代码]
+

增加查询条件,限制查询结果指定数组字段长度与查询值相同

+
+
参数
+
    +
  • key – 查询条件数组字段名

  • +
  • size – 查询条件值

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+skip(n)[源代码]
+

查询条件中跳过指定个数的对象,在做分页时很有帮助。

+
+
参数
+

n – 需要跳过对象的个数

+
+
返回类型
+

Query

+
+
+
+ +
+
+startswith(key, value)[源代码]
+

增加查询条件,限制查询结果对象指定最短的值,以指定字符串开头。在数据量比较大的情况下会比较慢。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • value – 需要查询的字符串

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+within_geo_box(key, southwest, northeast)[源代码]
+

增加查询条件,限制返回结果指定字段值的位置在指定坐标范围之内。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • southwest – 限制范围西南角坐标

  • +
  • northeast – 限制范围东北角坐标

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+within_kilometers(key, point, max_distance, min_distance=None)[源代码]
+

增加查询条件,限制返回结果指定字段值的位置在某点的一段距离之内。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • point – 查询地理位置

  • +
  • max_distance – 最大距离限定(千米)

  • +
  • min_distance – 最小距离限定(千米)

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+within_miles(key, point, max_distance, min_distance=None)[源代码]
+

增加查询条件,限制返回结果指定字段值的位置在某点的一段距离之内。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • point – 查询地理位置

  • +
  • max_distance – 最大距离限定(英里)

  • +
  • min_distance – 最小距离限定(英里)

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+
+within_radians(key, point, max_distance, min_distance=None)[源代码]
+

增加查询条件,限制返回结果指定字段值的位置在某点的一段距离之内。

+
+
参数
+
    +
  • key – 查询条件字段名

  • +
  • point – 查询地理位置

  • +
  • max_distance – 最大距离限定(弧度)

  • +
  • min_distance – 最小距离限定(弧度)

  • +
+
+
返回类型
+

Query

+
+
+
+ +
+ +
+
+

Relation

+
+
+class leancloud.Relation(parent, key=None)[源代码]
+

基类:object

+
+
+add(*obj_or_objs)[源代码]
+

添加一个新的 leancloud.Object 至 Relation。

+
+
参数
+

obj_or_objs – 需要添加的对象或对象列表

+
+
+
+ +
+
+dump()[源代码]
+
+ +
+
+property query
+

获取指向 Relation 内容的 Query 对象。

+
+
返回类型
+

leancloud.Query

+
+
+
+ +
+
+remove(*obj_or_objs)[源代码]
+

从一个 Relation 中删除一个 leancloud.Object 。

+
+
参数
+

obj_or_objs – 需要删除的对象或对象列表

+
+
返回
+

+
+
+
+ +
+
+classmethod reverse_query(parent_class, relation_key, child)[源代码]
+

创建一个新的 Query 对象,反向查询所有指向此 Relation 的父对象。

+
+
参数
+
    +
  • parent_class – 父类名称

  • +
  • relation_key – 父类中 Relation 的字段名

  • +
  • child – 子类对象

  • +
+
+
返回
+

leancloud.Query

+
+
+
+ +
+ +
+
+

Role

+
+
+class leancloud.Role(name=None, acl=None)[源代码]
+

基类:Object

+
+
+add(attr, item)
+

在对象此字段对应的数组末尾添加指定对象。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • item – 要添加的对象

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+add_unique(attr, item)
+

在对象此字段对应的数组末尾添加指定对象,如果此对象并没有包含在字段中。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • item – 要添加的对象

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+static as_class(arg)
+
+ +
+
+bit_and(attr, value)
+
+ +
+
+bit_or(attr, value)
+
+ +
+
+bit_xor(attr, value)
+
+ +
+
+clear()
+

将当前对象所有字段全部移除。

+
+
返回
+

当前对象

+
+
+
+ +
+
+classmethod create(class_name, **attributes)
+

根据参数创建一个 leancloud.Object 的子类的实例化对象

+
+
参数
+
    +
  • class_name (string_types) – 子类名称

  • +
  • attributes – 对象属性

  • +
+
+
返回
+

派生子类的实例

+
+
返回类型
+

Object

+
+
+
+ +
+
+classmethod create_without_data(id_)
+

根据 objectId 创建一个 leancloud.Object,代表一个服务器上已经存在的对象。可以调用 fetch 方法来获取服务器上的数据

+
+
参数
+

id (string_types) – 对象的 objectId

+
+
返回
+

没有数据的对象

+
+
返回类型
+

Object

+
+
+
+ +
+
+destroy()
+

从服务器上删除这个对象

+
+
返回类型
+

None

+
+
+
+ +
+
+classmethod destroy_all(objs)
+

在一个请求中 destroy 多个 leancloud.Object 对象实例。

+
+
参数
+

objs (list) – 需要 destroy 的对象

+
+
+
+ +
+
+disable_after_hook()
+
+ +
+
+disable_before_hook()
+
+ +
+
+dump()
+
+ +
+
+classmethod extend(name)
+

派生一个新的 leancloud.Object 子类

+
+
参数
+

name (string_types) – 子类名称

+
+
返回
+

派生的子类

+
+
返回类型
+

ObjectMeta

+
+
+
+ +
+
+fetch(select=None, include=None)
+

从服务器获取当前对象所有的值,如果与本地值不同,将会覆盖本地的值。

+
+
返回
+

当前对象

+
+
+
+ +
+
+get(attr, default=None, deafult=None)
+

获取对象字段的值

+
+
参数
+

attr (string_types) – 字段名

+
+
返回
+

字段值

+
+
+
+ +
+
+get_acl()
+

返回当前对象的 ACL。

+
+
返回
+

当前对象的 ACL

+
+
返回类型
+

leancloud.ACL

+
+
+
+ +
+
+get_name()[源代码]
+

获取 Role 的 name,等同于 role.get(‘name’)

+
+ +
+
+get_roles()[源代码]
+
+ +
+
+get_users()[源代码]
+

获取当前 Role 下所有绑定的用户。

+
+ +
+
+has(attr)
+

判断此字段是否有值

+
+
参数
+

attr – 字段名

+
+
返回
+

当有值时返回 True, 否则返回 False

+
+
返回类型
+

bool

+
+
+
+ +
+
+ignore_hook(hook_name)
+
+ +
+
+increment(attr, amount=1)
+

在对象此字段上自增对应的数值,如果数值没有指定,默认为一。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • amount – 自增量

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+is_dirty(attr=None)
+
+ +
+
+is_existed()
+

判断当前对象是否在服务器上已经存在。

+
+
返回类型
+

bool

+
+
+
+ +
+
+is_new()
+

判断当前对象是否已经保存至服务器。

+

该方法为 SDK 内部使用(save 调用此方法 dispatch 保存操作为 REST API 的 POST 和 PUT 请求)。 +查询对象是否在服务器上存在请使用 is_existed 方法。

+
+
返回类型
+

bool

+
+
+
+ +
+
+property name
+
+ +
+
+relation(attr)
+

返回对象上相应字段的 Relation

+
+
参数
+

attr (string_types) – 字段名

+
+
返回
+

Relation

+
+
返回类型
+

leancloud.Relation

+
+
+
+ +
+
+remove(attr, item)
+

在对象此字段对应的数组中,将指定对象全部移除。

+
+
参数
+
    +
  • attr – 字段名

  • +
  • item – 要移除的对象

  • +
+
+
返回
+

当前对象

+
+
+
+ +
+
+property roles
+
+ +
+
+save(where=None, fetch_when_save=None)
+

将对象数据保存至服务器

+
+
返回
+

None

+
+
返回类型
+

None

+
+
+
+ +
+
+classmethod save_all(objs)
+

在一个请求中 save 多个 leancloud.Object 对象实例。

+
+
参数
+

objs (list) – 需要 save 的对象

+
+
+
+ +
+
+set(key_or_attrs, value=None, unset=False)
+

在当前对象此字段上赋值

+
+
参数
+
    +
  • key_or_attrs (string_types or dict) – 字段名,或者一个包含 字段名 / 值的 dict

  • +
  • value – 字段值

  • +
  • unset

  • +
+
+
返回
+

当前对象,供链式调用

+
+
+
+ +
+
+set_acl(acl)
+

为当前对象设置 ACL

+
+
返回
+

当前对象

+
+
+
+ +
+
+set_name(name)[源代码]
+

为 Role 设置 name,等同于 role.set(‘name’, name)

+
+ +
+
+unset(attr)
+

在对象上移除此字段。

+
+
参数
+

attr – 字段名

+
+
返回
+

当前对象

+
+
+
+ +
+
+property users
+
+ +
+
+validate(attrs)[源代码]
+
+ +
+ +
+
+

ACL

+
+
+class leancloud.ACL(permissions_by_id=None)[源代码]
+

基类:object

+
+
+dump()[源代码]
+
+ +
+
+get_public_read_access()[源代码]
+
+ +
+
+get_public_write_access()[源代码]
+
+ +
+
+get_read_access(user_id)[源代码]
+
+ +
+
+get_role_read_access(role)[源代码]
+
+ +
+
+get_role_write_access(role)[源代码]
+
+ +
+
+get_write_access(user_id)[源代码]
+
+ +
+
+set_public_read_access(allowed)[源代码]
+
+ +
+
+set_public_write_access(allowed)[源代码]
+
+ +
+
+set_read_access(user_id, allowed)[源代码]
+
+ +
+
+set_role_read_access(role, allowed)[源代码]
+
+ +
+
+set_role_write_access(role, allowed)[源代码]
+
+ +
+
+set_write_access(user_id, allowed)[源代码]
+
+ +
+ +
+
+

GeoPoint

+
+
+class leancloud.GeoPoint(latitude=0, longitude=0)[源代码]
+

基类:object

+
+
+dump()[源代码]
+
+ +
+
+kilometers_to(other)[源代码]
+

Returns the distance from this GeoPoint to another in kilometers.

+
+
参数
+

other (GeoPoint) – point the other GeoPoint

+
+
返回类型
+

float

+
+
+
+ +
+
+property latitude
+

当前对象的纬度

+
+ +
+
+property longitude
+

当前对象的经度

+
+ +
+
+miles_to(other)[源代码]
+

Returns the distance from this GeoPoint to another in miles.

+
+
参数
+

other (GeoPoint) – point the other GeoPoint

+
+
返回类型
+

float

+
+
+
+ +
+
+radians_to(other)[源代码]
+

Returns the distance from this GeoPoint to another in radians.

+
+
参数
+

other (GeoPoint) – point the other GeoPoint

+
+
返回类型
+

float

+
+
+
+ +
+ +
+
+

Engine

+
+
+class leancloud.Engine(wsgi_app=None, fetch_user=True)[源代码]
+

基类:object

+

LeanEngine middleware.

+
+
+after_delete(*args, **kwargs)[源代码]
+
+ +
+
+after_save(*args, **kwargs)[源代码]
+
+ +
+
+after_update(*args, **kwargs)[源代码]
+
+ +
+
+before_delete(*args, **kwargs)[源代码]
+
+ +
+
+before_save(*args, **kwargs)[源代码]
+
+ +
+
+before_update(*args, **kwargs)[源代码]
+
+ +
+
+define(*args, **kwargs)[源代码]
+
+ +
+
+on_auth_data(*args, **kwargs)[源代码]
+
+ +
+
+on_bigquery(*args, **kwargs)[源代码]
+
+ +
+
+on_insight(*args, **kwargs)[源代码]
+
+ +
+
+on_login(*args, **kwargs)[源代码]
+
+ +
+
+on_verified(*args, **kwargs)[源代码]
+
+ +
+
+register(engine)[源代码]
+
+ +
+
+run(*args, **kwargs)[源代码]
+
+ +
+
+start()[源代码]
+
+ +
+
+stop()[源代码]
+
+ +
+
+wrap(wsgi_app)[源代码]
+
+ +
+ +
+
+

HttpsRedirectMiddleware

+
+
+class leancloud.engine.HttpsRedirectMiddleware(wsgi_app)[源代码]
+

基类:object

+
+ +
+
+

CookieSessionMiddleware

+
+
+class leancloud.engine.CookieSessionMiddleware(app, secret, name='leancloud:session', excluded_paths=None, fetch_user=False, expires=None, max_age=None)[源代码]
+

基类:object

+

用来在 webhosting 功能中实现自动管理 LeanCloud 用户登录状态的 WSGI 中间件。 +使用此中间件之后,在处理 web 请求中调用了 leancloud.User.login() 方法登录成功后, +会将此用户 session token 写入到 cookie 中。 +后续此次会话都可以通过 leancloud.User.get_current() 获取到此用户对象。

+
+
参数
+
    +
  • secret (str) – 对保存在 cookie 中的用户 session token 进行签名时需要的 key,可使用任意方法随机生成,请不要泄漏

  • +
  • name (str) – 在 cookie 中保存的 session token 的 key 的名称,默认为 “leancloud:session”

  • +
  • excluded_paths (list) – 指定哪些 URL path 不处理 session token,比如在处理静态文件的 URL path 上不进行处理,防止无谓的性能浪费

  • +
  • fetch_user (bool) – 处理请求时是否要从存储服务获取用户数据, +如果为 false 的话, +leancloud.User.get_current() 获取到的用户数据上除了 session_token 之外没有任何其他数据, +需要自己调用 fetch() 来获取。 +为 true 的话,会自动在用户对象上调用 fetch(),这样将会产生一次数据存储的 API 调用。 +默认为 false

  • +
  • expires (int or datetime) – 设置 cookie 的 expires

  • +
  • max_age (int) – 设置 cookie 的 max_age,单位为秒

  • +
+
+
+
+
+post_process(environ, headers)[源代码]
+
+ +
+
+pre_process(environ)[源代码]
+
+ +
+ +
+
+
+

leancloud.push

+
+
+class leancloud.push.Installation(**attrs)[源代码]
+

基类:Object

+
+ +
+
+class leancloud.push.Notification(**attrs)[源代码]
+

基类:Object

+
+
+fetch(*args, **kwargs)[源代码]
+

同步服务器的 Notification 数据

+
+ +
+
+save(*args, **kwargs)[源代码]
+

将对象数据保存至服务器

+
+
返回
+

None

+
+
返回类型
+

None

+
+
+
+ +
+ +
+
+leancloud.push.send(data, channels=None, push_time=None, expiration_time=None, expiration_interval=None, where=None, cql=None, flow_control=None, prod=None)[源代码]
+

发送推送消息。返回结果为此条推送对应的 _Notification 表中的对象,但是如果需要使用其中的数据,需要调用 fetch() 方法将数据同步至本地。

+
+
参数
+
    +
  • channels (list or tuple) – 需要推送的频道

  • +
  • push_time (datetime) – 推送的时间

  • +
  • expiration_time (datetime) – 消息过期的绝对日期时间

  • +
  • expiration_interval (int) – 消息过期的相对时间,从调用 API 的时间开始算起,单位是秒

  • +
  • where (leancloud.Query) – 一个查询 _Installation 表的查询条件 leancloud.Query 对象

  • +
  • cql (string_types) – 一个查询 _Installation 表的查询条件 CQL 语句

  • +
  • data – 推送给设备的具体信息,详情查看 https://leancloud.cn/docs/push_guide.html#消息内容_Data

  • +
  • flow_control – 不为 None 时开启平滑推送,值为每秒推送的目标终端用户数。开启时指定低于 1000 的值,按 1000 计。

  • +
  • prod – 仅对 iOS 推送有效,设置将推送发至 APNs 的开发环境(dev)还是生产环境(prod)。

  • +
+
+
返回类型
+

Notification

+
+
Type
+

flow_control: int

+
+
Type
+

prod: string

+
+
+
+ +
+
+

leancloud.cloud

+
+
+class leancloud.cloud.Captcha(token, url)[源代码]
+

基类:object

+

表示图形验证码

+
+
+verify(code)[源代码]
+

验证用户输入与图形验证码是否匹配 +:params code: 用户填写的验证码

+
+ +
+ +
+
+leancloud.cloud.get_server_time()[源代码]
+
+ +
+
+leancloud.cloud.request_captcha(size=None, width=None, height=None, ttl=None)[源代码]
+

请求生成新的图形验证码 +:return: Captcha

+
+ +
+
+leancloud.cloud.request_sms_code(phone_number, idd='+86', sms_type='sms', validate_token=None, template=None, sign=None, params=None)[源代码]
+

请求发送手机验证码 +:param phone_number: 需要验证的手机号码 +:param idd: 号码的所在地国家代码,默认为中国(+86) +:param sms_type: 验证码发送方式,’voice’ 为语音,’sms’ 为短信 +:param template: 模版名称 +:param sign: 短信签名名称 +:return: None

+
+ +
+
+leancloud.cloud.rpc(_cloud_rpc_name, **params)[源代码]
+

调用 LeanEngine 上的远程代码 +与 cloud.run 类似,但是允许传入 leancloud.Object 作为参数,也允许传入 leancloud.Object 作为结果 +:param name: 需要调用的远程 Cloud Code 的名称 +:type name: basestring +:param params: 调用参数 +:return: 调用结果

+
+ +
+
+leancloud.cloud.run(_cloud_func_name, **params)[源代码]
+

调用 LeanEngine 上的远程代码 +:param name: 需要调用的远程 Cloud Code 的名称 +:type name: string_types +:param params: 调用参数 +:return: 调用结果

+
+ +
+
+leancloud.cloud.verify_captcha(code, token)[源代码]
+

验证用户输入与图形验证码是否匹配 +:params code: 用户填写的验证码 +:params token: 图形验证码对应的 token +:return: validate token

+
+ +
+
+leancloud.cloud.verify_sms_code(phone_number, code)[源代码]
+

获取到手机验证码之后,验证验证码是否正确。如果验证失败,抛出异常。 +:param phone_number: 需要验证的手机号码 +:param code: 接受到的验证码 +:return: None

+
+ +
+
+

Indices and tables

+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 1468af9e..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. LeanCloud-Python-SDK documentation master file, created by - sphinx-quickstart on Wed Mar 18 13:20:31 2015. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -LeanCloud-Python-SDK API 文档 -================================================ - - -.. toctree:: - :maxdepth: 2 - - leancloud - leancloud.engine - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/leancloud.acl.rst b/docs/leancloud.acl.rst deleted file mode 100644 index 44513cb2..00000000 --- a/docs/leancloud.acl.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.acl module -==================== - -.. automodule:: leancloud.acl - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.client.rst b/docs/leancloud.client.rst deleted file mode 100644 index c2715bbe..00000000 --- a/docs/leancloud.client.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.client module -======================= - -.. automodule:: leancloud.client - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.engine.rst b/docs/leancloud.engine.rst deleted file mode 100644 index 6d789211..00000000 --- a/docs/leancloud.engine.rst +++ /dev/null @@ -1,54 +0,0 @@ -leancloud.engine package -======================== - -Submodules ----------- - -leancloud.engine.authorization module -------------------------------------- - -.. automodule:: leancloud.engine.authorization - :members: - :undoc-members: - :show-inheritance: - -leancloud.engine.cloudfunc module ---------------------------------- - -.. automodule:: leancloud.engine.cloudfunc - :members: - :undoc-members: - :show-inheritance: - -leancloud.engine.context module -------------------------------- - -.. automodule:: leancloud.engine.context - :members: - :undoc-members: - :show-inheritance: - -leancloud.engine.leanengine module ----------------------------------- - -.. automodule:: leancloud.engine.leanengine - :members: - :undoc-members: - :show-inheritance: - -leancloud.engine.utils module ------------------------------ - -.. automodule:: leancloud.engine.utils - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: leancloud.engine - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.errors.rst b/docs/leancloud.errors.rst deleted file mode 100644 index 3e720ed8..00000000 --- a/docs/leancloud.errors.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.errors module -======================= - -.. automodule:: leancloud.errors - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.fields.rst b/docs/leancloud.fields.rst deleted file mode 100644 index d2828184..00000000 --- a/docs/leancloud.fields.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.fields module -======================= - -.. automodule:: leancloud.fields - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.file_.rst b/docs/leancloud.file_.rst deleted file mode 100644 index 64b60a72..00000000 --- a/docs/leancloud.file_.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.file_ module -====================== - -.. automodule:: leancloud.file_ - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.geo_point.rst b/docs/leancloud.geo_point.rst deleted file mode 100644 index a8f8d2fb..00000000 --- a/docs/leancloud.geo_point.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.geo_point module -========================== - -.. automodule:: leancloud.geo_point - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.mime_type.rst b/docs/leancloud.mime_type.rst deleted file mode 100644 index 97aadb8b..00000000 --- a/docs/leancloud.mime_type.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.mime_type module -========================== - -.. automodule:: leancloud.mime_type - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.object_.rst b/docs/leancloud.object_.rst deleted file mode 100644 index 0a149537..00000000 --- a/docs/leancloud.object_.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.object_ module -======================== - -.. automodule:: leancloud.object_ - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.operation.rst b/docs/leancloud.operation.rst deleted file mode 100644 index 94c5ad08..00000000 --- a/docs/leancloud.operation.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.operation module -========================== - -.. automodule:: leancloud.operation - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.push.rst b/docs/leancloud.push.rst deleted file mode 100644 index adef7598..00000000 --- a/docs/leancloud.push.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.push module -==================== - -.. automodule:: leancloud.push - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.query.rst b/docs/leancloud.query.rst deleted file mode 100644 index 8bee3bb3..00000000 --- a/docs/leancloud.query.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.query module -====================== - -.. automodule:: leancloud.query - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.relation.rst b/docs/leancloud.relation.rst deleted file mode 100644 index 10eae3b3..00000000 --- a/docs/leancloud.relation.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.relation module -========================= - -.. automodule:: leancloud.relation - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.role.rst b/docs/leancloud.role.rst deleted file mode 100644 index ca6d9494..00000000 --- a/docs/leancloud.role.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.role module -===================== - -.. automodule:: leancloud.role - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.rst b/docs/leancloud.rst deleted file mode 100644 index 75e7eabb..00000000 --- a/docs/leancloud.rst +++ /dev/null @@ -1,24 +0,0 @@ -leancloud package -================= - -Submodules ----------- - -.. toctree:: - - leancloud.acl - leancloud.client - leancloud.errors - leancloud.fields - leancloud.file_ - leancloud.geo_point - leancloud.mime_type - leancloud.object_ - leancloud.operation - leancloud.push - leancloud.query - leancloud.relation - leancloud.role - leancloud.user - leancloud.utils - diff --git a/docs/leancloud.user.rst b/docs/leancloud.user.rst deleted file mode 100644 index b3f0f708..00000000 --- a/docs/leancloud.user.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.user module -===================== - -.. automodule:: leancloud.user - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/leancloud.utils.rst b/docs/leancloud.utils.rst deleted file mode 100644 index 464c26bc..00000000 --- a/docs/leancloud.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud.utils module -====================== - -.. automodule:: leancloud.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/modules.rst b/docs/modules.rst deleted file mode 100644 index 5340144b..00000000 --- a/docs/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -leancloud -========= - -.. toctree:: - :maxdepth: 4 - - leancloud diff --git a/docs/objects.inv b/docs/objects.inv new file mode 100644 index 00000000..39be7748 Binary files /dev/null and b/docs/objects.inv differ diff --git a/docs/py-modindex.html b/docs/py-modindex.html new file mode 100644 index 00000000..27db31e0 --- /dev/null +++ b/docs/py-modindex.html @@ -0,0 +1,127 @@ + + + + + + Python 模块索引 — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • Python 模块索引
  • +
  • +
  • +
+
+
+
+
+ + +

Python 模块索引

+ +
+ l +
+ + + + + + + + + + + + + +
 
+ l
+ leancloud +
    + leancloud.cloud +
    + leancloud.push +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/search.html b/docs/search.html new file mode 100644 index 00000000..4bd6098f --- /dev/null +++ b/docs/search.html @@ -0,0 +1,117 @@ + + + + + + 搜索 — LeanCloud-Python-SDK 2.6.1 文档 + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • 搜索
  • +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js new file mode 100644 index 00000000..6e3b5444 --- /dev/null +++ b/docs/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"docnames": ["index"], "filenames": ["index.rst"], "titles": ["LeanCloud-Python-SDK API \u6587\u6863"], "terms": {"init": 0, "app_id": 0, "app_kei": 0, "none": 0, "master_kei": 0, "hook_kei": 0, "appid": 0, "appkei": 0, "masterkei": 0, "string_typ": 0, "applic": 0, "id": 0, "or": 0, "kei": 0, "master": 0, "hook": 0, "use_master_kei": 0, "flag": 0, "true": 0, "use_product": 0, "func": 0, "use_region": 0, "region": 0, "class": 0, "friendshipqueri": 0, "query_class": 0, "leanclouderror": 0, "code": 0, "error": 0, "except": 0, "leancloudwarn": 0, "userwarn": 0, "attr": 0, "add": 0, "item": 0, "add_uniqu": 0, "static": 0, "as_class": 0, "arg": 0, "bit_and": 0, "valu": 0, "bit_or": 0, "bit_xor": 0, "clear": 0, "classmethod": 0, "creat": 0, "class_nam": 0, "attribut": 0, "create_without_data": 0, "id_": 0, "objectid": 0, "fetch": 0, "destroi": 0, "destroy_al": 0, "obj": 0, "list": 0, "disable_after_hook": 0, "disable_before_hook": 0, "dump": 0, "extend": 0, "name": 0, "objectmeta": 0, "select": 0, "includ": 0, "get": 0, "default": 0, "deafult": 0, "get_acl": 0, "has": 0, "fals": 0, "bool": 0, "ignore_hook": 0, "hook_nam": 0, "increment": 0, "amount": 0, "is_dirti": 0, "is_exist": 0, "is_new": 0, "save": 0, "dispatch": 0, "rest": 0, "post": 0, "put": 0, "remov": 0, "where": 0, "fetch_when_sav": 0, "save_al": 0, "set": 0, "key_or_attr": 0, "unset": 0, "dict": 0, "set_acl": 0, "valid": 0, "becom": 0, "session_token": 0, "session": 0, "token": 0, "change_phone_numb": 0, "sms_code": 0, "phone_numb": 0, "create_followee_queri": 0, "user_id": 0, "create_follower_queri": 0, "follow": 0, "target_id": 0, "get_curr": 0, "option": 0, "get_email": 0, "get_mobile_phone_numb": 0, "get_rol": 0, "get_session_token": 0, "get_usernam": 0, "is_authent": 0, "properti": 0, "is_curr": 0, "is_link": 0, "provid": 0, "link_with": 0, "third_party_auth_data": 0, "login": 0, "usernam": 0, "password": 0, "email": 0, "sessiontoken": 0, "login_with": 0, "platform": 0, "param": 0, "base": 0, "string": 0, "login_with_mobile_phon": 0, "logout": 0, "refresh_session_token": 0, "request_change_phone_numb": 0, "ttl": 0, "validate_token": 0, "request_email_verifi": 0, "request_login_sms_cod": 0, "request_mobile_phone_verifi": 0, "request_password_reset": 0, "request_password_reset_by_sms_cod": 0, "reset_password_by_sms_cod": 0, "new_password": 0, "make_curr": 0, "set_curr": 0, "set_email": 0, "set_mobile_phone_numb": 0, "set_password": 0, "set_usernam": 0, "sign_up": 0, "signup_or_login_with_mobile_phon": 0, "phone_nubm": 0, "request_sms_cod": 0, "sms": 0, "unfollow": 0, "unlink_from": 0, "update_password": 0, "old_password": 0, "verify_mobile_phone_numb": 0, "data": 0, "mime_typ": 0, "create_with_url": 0, "url": 0, "meta_data": 0, "object_id": 0, "get_thumbnail_url": 0, "width": 0, "height": 0, "qualiti": 0, "100": 0, "scale_to_fit": 0, "fmt": 0, "png": 0, "metadata": 0, "owner_id": 0, "set_mime_typ": 0, "size": 0, "add_ascend": 0, "add_descend": 0, "and_": 0, "ascend": 0, "contained_in": 0, "tupl": 0, "contain": 0, "contains_al": 0, "count": 0, "int": 0, "descend": 0, "do_cloud_queri": 0, "cql": 0, "pvalu": 0, "cqlresult": 0, "does_not_exist": 0, "does_not_match_key_in_queri": 0, "query_kei": 0, "does_not_match_queri": 0, "endswith": 0, "equal_to": 0, "exist": 0, "find": 0, "first": 0, "rais": 0, "greater_than": 0, "greater_than_or_equal_to": 0, "include_acl": 0, "less_than": 0, "less_than_or_equal_to": 0, "limit": 0, "1000": 0, "match": 0, "regex": 0, "ignore_cas": 0, "multi_lin": 0, "matches_key_in_queri": 0, "matches_queri": 0, "near": 0, "point": 0, "not_contained_in": 0, "not_equal_to": 0, "or_": 0, "scan": 0, "batch_siz": 0, "scan_kei": 0, "size_equal_to": 0, "skip": 0, "startswith": 0, "within_geo_box": 0, "southwest": 0, "northeast": 0, "within_kilomet": 0, "max_dist": 0, "min_dist": 0, "within_mil": 0, "within_radian": 0, "parent": 0, "obj_or_obj": 0, "reverse_queri": 0, "parent_class": 0, "relation_kei": 0, "child": 0, "get_nam": 0, "get_us": 0, "set_nam": 0, "permissions_by_id": 0, "get_public_read_access": 0, "get_public_write_access": 0, "get_read_access": 0, "get_role_read_access": 0, "get_role_write_access": 0, "get_write_access": 0, "set_public_read_access": 0, "allow": 0, "set_public_write_access": 0, "set_read_access": 0, "set_role_read_access": 0, "set_role_write_access": 0, "set_write_access": 0, "latitud": 0, "longitud": 0, "kilometers_to": 0, "other": 0, "return": 0, "the": 0, "distanc": 0, "from": 0, "thi": 0, "to": 0, "anoth": 0, "in": 0, "kilomet": 0, "float": 0, "miles_to": 0, "mile": 0, "radians_to": 0, "radian": 0, "wsgi_app": 0, "fetch_us": 0, "leanengin": 0, "middlewar": 0, "after_delet": 0, "kwarg": 0, "after_sav": 0, "after_upd": 0, "before_delet": 0, "before_sav": 0, "before_upd": 0, "defin": 0, "on_auth_data": 0, "on_bigqueri": 0, "on_insight": 0, "on_login": 0, "on_verifi": 0, "regist": 0, "run": 0, "start": 0, "stop": 0, "wrap": 0, "app": 0, "secret": 0, "excluded_path": 0, "expir": 0, "max_ag": 0, "webhost": 0, "wsgi": 0, "web": 0, "cooki": 0, "str": 0, "path": 0, "datetim": 0, "post_process": 0, "environ": 0, "header": 0, "pre_process": 0, "instal": 0, "notif": 0, "send": 0, "channel": 0, "push_tim": 0, "expiration_tim": 0, "expiration_interv": 0, "flow_control": 0, "prod": 0, "_notif": 0, "_instal": 0, "http": 0, "cn": 0, "doc": 0, "push_guid": 0, "html": 0, "_data": 0, "ios": 0, "apn": 0, "dev": 0, "type": 0, "captcha": 0, "verifi": 0, "get_server_tim": 0, "request_captcha": 0, "idd": 0, "86": 0, "sms_type": 0, "templat": 0, "sign": 0, "voic": 0, "rpc": 0, "_cloud_rpc_nam": 0, "basestr": 0, "_cloud_func_nam": 0, "verify_captcha": 0, "verify_sms_cod": 0}, "objects": {"leancloud": [[0, 0, 1, "", "ACL"], [0, 0, 1, "", "Engine"], [0, 0, 1, "", "File"], [0, 0, 1, "", "FriendshipQuery"], [0, 0, 1, "", "GeoPoint"], [0, 0, 1, "", "LeanCloudError"], [0, 0, 1, "", "LeanCloudWarning"], [0, 0, 1, "", "Object"], [0, 0, 1, "", "Query"], [0, 0, 1, "", "Relation"], [0, 0, 1, "", "Role"], [0, 0, 1, "", "User"], [0, 4, 0, "-", "cloud"], [0, 5, 1, "", "init"], [0, 4, 0, "-", "push"], [0, 5, 1, "", "use_master_key"], [0, 5, 1, "", "use_production"], [0, 5, 1, "", "use_region"]], "leancloud.ACL": [[0, 1, 1, "", "dump"], [0, 1, 1, "", "get_public_read_access"], [0, 1, 1, "", "get_public_write_access"], [0, 1, 1, "", "get_read_access"], [0, 1, 1, "", "get_role_read_access"], [0, 1, 1, "", "get_role_write_access"], [0, 1, 1, "", "get_write_access"], [0, 1, 1, "", "set_public_read_access"], [0, 1, 1, "", "set_public_write_access"], [0, 1, 1, "", "set_read_access"], [0, 1, 1, "", "set_role_read_access"], [0, 1, 1, "", "set_role_write_access"], [0, 1, 1, "", "set_write_access"]], "leancloud.Engine": [[0, 1, 1, "", "after_delete"], [0, 1, 1, "", "after_save"], [0, 1, 1, "", "after_update"], [0, 1, 1, "", "before_delete"], [0, 1, 1, "", "before_save"], [0, 1, 1, "", "before_update"], [0, 1, 1, "", "define"], [0, 1, 1, "", "on_auth_data"], [0, 1, 1, "", "on_bigquery"], [0, 1, 1, "", "on_insight"], [0, 1, 1, "", "on_login"], [0, 1, 1, "", "on_verified"], [0, 1, 1, "", "register"], [0, 1, 1, "", "run"], [0, 1, 1, "", "start"], [0, 1, 1, "", "stop"], [0, 1, 1, "", "wrap"]], "leancloud.File": [[0, 1, 1, "", "create_with_url"], [0, 1, 1, "", "create_without_data"], [0, 1, 1, "", "destroy"], [0, 1, 1, "", "fetch"], [0, 1, 1, "", "get_acl"], [0, 1, 1, "", "get_thumbnail_url"], [0, 2, 1, "", "metadata"], [0, 2, 1, "", "mime_type"], [0, 2, 1, "", "name"], [0, 2, 1, "", "owner_id"], [0, 3, 1, "", "query"], [0, 1, 1, "", "save"], [0, 1, 1, "", "set_acl"], [0, 2, 1, "", "set_mime_type"], [0, 2, 1, "", "size"], [0, 2, 1, "", "url"]], "leancloud.GeoPoint": [[0, 1, 1, "", "dump"], [0, 1, 1, "", "kilometers_to"], [0, 2, 1, "", "latitude"], [0, 2, 1, "", "longitude"], [0, 1, 1, "", "miles_to"], [0, 1, 1, "", "radians_to"]], "leancloud.Object": [[0, 1, 1, "", "add"], [0, 1, 1, "", "add_unique"], [0, 1, 1, "", "as_class"], [0, 1, 1, "", "bit_and"], [0, 1, 1, "", "bit_or"], [0, 1, 1, "", "bit_xor"], [0, 1, 1, "", "clear"], [0, 1, 1, "", "create"], [0, 1, 1, "", "create_without_data"], [0, 1, 1, "", "destroy"], [0, 1, 1, "", "destroy_all"], [0, 1, 1, "", "disable_after_hook"], [0, 1, 1, "", "disable_before_hook"], [0, 1, 1, "", "dump"], [0, 1, 1, "", "extend"], [0, 1, 1, "", "fetch"], [0, 1, 1, "", "get"], [0, 1, 1, "", "get_acl"], [0, 1, 1, "", "has"], [0, 1, 1, "", "ignore_hook"], [0, 1, 1, "", "increment"], [0, 1, 1, "", "is_dirty"], [0, 1, 1, "", "is_existed"], [0, 1, 1, "", "is_new"], [0, 1, 1, "", "relation"], [0, 1, 1, "", "remove"], [0, 1, 1, "", "save"], [0, 1, 1, "", "save_all"], [0, 1, 1, "", "set"], [0, 1, 1, "", "set_acl"], [0, 1, 1, "", "unset"], [0, 1, 1, "", "validate"]], "leancloud.Query": [[0, 1, 1, "", "add_ascending"], [0, 1, 1, "", "add_descending"], [0, 1, 1, "", "and_"], [0, 1, 1, "", "ascending"], [0, 1, 1, "", "contained_in"], [0, 1, 1, "", "contains"], [0, 1, 1, "", "contains_all"], [0, 1, 1, "", "count"], [0, 1, 1, "", "descending"], [0, 1, 1, "", "do_cloud_query"], [0, 1, 1, "", "does_not_exist"], [0, 1, 1, "", "does_not_match_key_in_query"], [0, 1, 1, "", "does_not_match_query"], [0, 1, 1, "", "dump"], [0, 1, 1, "", "endswith"], [0, 1, 1, "", "equal_to"], [0, 1, 1, "", "exists"], [0, 1, 1, "", "find"], [0, 1, 1, "", "first"], [0, 1, 1, "", "get"], [0, 1, 1, "", "greater_than"], [0, 1, 1, "", "greater_than_or_equal_to"], [0, 1, 1, "", "include"], [0, 1, 1, "", "include_acl"], [0, 1, 1, "", "less_than"], [0, 1, 1, "", "less_than_or_equal_to"], [0, 1, 1, "", "limit"], [0, 1, 1, "", "matched"], [0, 1, 1, "", "matches_key_in_query"], [0, 1, 1, "", "matches_query"], [0, 1, 1, "", "near"], [0, 1, 1, "", "not_contained_in"], [0, 1, 1, "", "not_equal_to"], [0, 1, 1, "", "or_"], [0, 1, 1, "", "scan"], [0, 1, 1, "", "select"], [0, 1, 1, "", "size_equal_to"], [0, 1, 1, "", "skip"], [0, 1, 1, "", "startswith"], [0, 1, 1, "", "within_geo_box"], [0, 1, 1, "", "within_kilometers"], [0, 1, 1, "", "within_miles"], [0, 1, 1, "", "within_radians"]], "leancloud.Relation": [[0, 1, 1, "", "add"], [0, 1, 1, "", "dump"], [0, 2, 1, "", "query"], [0, 1, 1, "", "remove"], [0, 1, 1, "", "reverse_query"]], "leancloud.Role": [[0, 1, 1, "", "add"], [0, 1, 1, "", "add_unique"], [0, 1, 1, "", "as_class"], [0, 1, 1, "", "bit_and"], [0, 1, 1, "", "bit_or"], [0, 1, 1, "", "bit_xor"], [0, 1, 1, "", "clear"], [0, 1, 1, "", "create"], [0, 1, 1, "", "create_without_data"], [0, 1, 1, "", "destroy"], [0, 1, 1, "", "destroy_all"], [0, 1, 1, "", "disable_after_hook"], [0, 1, 1, "", "disable_before_hook"], [0, 1, 1, "", "dump"], [0, 1, 1, "", "extend"], [0, 1, 1, "", "fetch"], [0, 1, 1, "", "get"], [0, 1, 1, "", "get_acl"], [0, 1, 1, "", "get_name"], [0, 1, 1, "", "get_roles"], [0, 1, 1, "", "get_users"], [0, 1, 1, "", "has"], [0, 1, 1, "", "ignore_hook"], [0, 1, 1, "", "increment"], [0, 1, 1, "", "is_dirty"], [0, 1, 1, "", "is_existed"], [0, 1, 1, "", "is_new"], [0, 2, 1, "", "name"], [0, 1, 1, "", "relation"], [0, 1, 1, "", "remove"], [0, 2, 1, "", "roles"], [0, 1, 1, "", "save"], [0, 1, 1, "", "save_all"], [0, 1, 1, "", "set"], [0, 1, 1, "", "set_acl"], [0, 1, 1, "", "set_name"], [0, 1, 1, "", "unset"], [0, 2, 1, "", "users"], [0, 1, 1, "", "validate"]], "leancloud.User": [[0, 1, 1, "", "add"], [0, 1, 1, "", "add_unique"], [0, 1, 1, "", "as_class"], [0, 1, 1, "", "become"], [0, 1, 1, "", "bit_and"], [0, 1, 1, "", "bit_or"], [0, 1, 1, "", "bit_xor"], [0, 1, 1, "", "change_phone_number"], [0, 1, 1, "", "clear"], [0, 1, 1, "", "create"], [0, 1, 1, "", "create_followee_query"], [0, 1, 1, "", "create_follower_query"], [0, 1, 1, "", "create_without_data"], [0, 1, 1, "", "destroy"], [0, 1, 1, "", "destroy_all"], [0, 1, 1, "", "disable_after_hook"], [0, 1, 1, "", "disable_before_hook"], [0, 1, 1, "", "dump"], [0, 1, 1, "", "extend"], [0, 1, 1, "", "fetch"], [0, 1, 1, "", "follow"], [0, 1, 1, "", "get"], [0, 1, 1, "", "get_acl"], [0, 1, 1, "", "get_current"], [0, 1, 1, "", "get_email"], [0, 1, 1, "", "get_mobile_phone_number"], [0, 1, 1, "", "get_roles"], [0, 1, 1, "", "get_session_token"], [0, 1, 1, "", "get_username"], [0, 1, 1, "", "has"], [0, 1, 1, "", "ignore_hook"], [0, 1, 1, "", "increment"], [0, 1, 1, "", "is_authenticated"], [0, 2, 1, "", "is_current"], [0, 1, 1, "", "is_dirty"], [0, 1, 1, "", "is_existed"], [0, 1, 1, "", "is_linked"], [0, 1, 1, "", "is_new"], [0, 1, 1, "", "link_with"], [0, 1, 1, "", "login"], [0, 1, 1, "", "login_with"], [0, 1, 1, "", "login_with_mobile_phone"], [0, 1, 1, "", "logout"], [0, 1, 1, "", "refresh_session_token"], [0, 1, 1, "", "relation"], [0, 1, 1, "", "remove"], [0, 1, 1, "", "request_change_phone_number"], [0, 1, 1, "", "request_email_verify"], [0, 1, 1, "", "request_login_sms_code"], [0, 1, 1, "", "request_mobile_phone_verify"], [0, 1, 1, "", "request_password_reset"], [0, 1, 1, "", "request_password_reset_by_sms_code"], [0, 1, 1, "", "reset_password_by_sms_code"], [0, 1, 1, "", "save"], [0, 1, 1, "", "save_all"], [0, 2, 1, "", "session_token"], [0, 1, 1, "", "set"], [0, 1, 1, "", "set_acl"], [0, 1, 1, "", "set_current"], [0, 1, 1, "", "set_email"], [0, 1, 1, "", "set_mobile_phone_number"], [0, 1, 1, "", "set_password"], [0, 1, 1, "", "set_username"], [0, 1, 1, "", "sign_up"], [0, 1, 1, "", "signup_or_login_with_mobile_phone"], [0, 1, 1, "", "unfollow"], [0, 1, 1, "", "unlink_from"], [0, 1, 1, "", "unset"], [0, 1, 1, "", "update_password"], [0, 1, 1, "", "validate"], [0, 1, 1, "", "verify_mobile_phone_number"]], "leancloud.cloud": [[0, 0, 1, "", "Captcha"], [0, 5, 1, "", "get_server_time"], [0, 5, 1, "", "request_captcha"], [0, 5, 1, "", "request_sms_code"], [0, 5, 1, "", "rpc"], [0, 5, 1, "", "run"], [0, 5, 1, "", "verify_captcha"], [0, 5, 1, "", "verify_sms_code"]], "leancloud.cloud.Captcha": [[0, 1, 1, "", "verify"]], "leancloud.engine": [[0, 0, 1, "", "CookieSessionMiddleware"], [0, 0, 1, "", "HttpsRedirectMiddleware"]], "leancloud.engine.CookieSessionMiddleware": [[0, 1, 1, "", "post_process"], [0, 1, 1, "", "pre_process"]], "leancloud.push": [[0, 0, 1, "", "Installation"], [0, 0, 1, "", "Notification"], [0, 5, 1, "", "send"]], "leancloud.push.Notification": [[0, 1, 1, "", "fetch"], [0, 1, 1, "", "save"]]}, "objtypes": {"0": "py:class", "1": "py:method", "2": "py:property", "3": "py:attribute", "4": "py:module", "5": "py:function"}, "objnames": {"0": ["py", "class", "Python \u7c7b"], "1": ["py", "method", "Python \u65b9\u6cd5"], "2": ["py", "property", "Python property"], "3": ["py", "attribute", "Python \u5c5e\u6027"], "4": ["py", "module", "Python \u6a21\u5757"], "5": ["py", "function", "Python \u51fd\u6570"]}, "titleterms": {"leancloud": 0, "python": 0, "sdk": 0, "api": 0, "object": 0, "user": 0, "file": 0, "queri": 0, "relat": 0, "role": 0, "acl": 0, "geopoint": 0, "engin": 0, "httpsredirectmiddlewar": 0, "cookiesessionmiddlewar": 0, "push": 0, "cloud": 0, "indic": 0, "and": 0, "tabl": 0}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.viewcode": 1, "sphinx": 56}}) \ No newline at end of file diff --git a/leancloud/__init__.py b/leancloud/__init__.py index dfd395b9..7ba5a59c 100644 --- a/leancloud/__init__.py +++ b/leancloud/__init__.py @@ -7,64 +7,72 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals -import sys import logging +import sys import warnings -logger = logging.getLogger('iso8601.iso8601') -logger.setLevel(logging.CRITICAL) - from . import client +from . import cloud from . import push from .acl import ACL from .client import init from .client import use_master_key from .client import use_production from .client import use_region +from .conversation import Conversation # noqa: F401 from .engine import Engine from .engine import LeanEngineError -from .engine import cloudfunc from .engine.https_redirect_middleware import HttpsRedirectMiddleware from .errors import LeanCloudError from .errors import LeanCloudWarning from .file_ import File from .geo_point import GeoPoint +from .message import Message # noqa: F401 from .object_ import Object from .push import Installation from .query import FriendshipQuery from .query import Query from .relation import Relation from .role import Role +from .status import InboxQuery +from .status import Status +from .sys_message import SysMessage from .user import User +logger = logging.getLogger("iso8601.iso8601") +logger.setLevel(logging.CRITICAL) -__author__ = 'asaka ' -__version__ = '1.6.1' +__author__ = "asaka " +__version__ = "2.1.9" __all__ = [ - 'ACL', - 'Engine', - 'File', - 'FriendshipQuery', - 'GeoPoint', - 'HttpsRedirectMiddleware', - 'Installation', - 'LeanCloudError', - 'LeanEngineError', - 'Object', - 'Query', - 'Relation', - 'Role', - 'User', - 'client', - 'cloudfunc', - 'init', - 'push', - 'use_master_key', - 'use_production', - 'use_region', + "ACL", + "Engine", + "File", + "FriendshipQuery", + "GeoPoint", + "HttpsRedirectMiddleware", + "InboxQuery", + "Installation", + "LeanCloudError", + "LeanEngineError", + "Object", + "Query", + "Relation", + "Role", + "Status", + "SysMessage", + "User", + "client", + "cloud", + "init", + "push", + "use_master_key", + "use_production", + "use_region", ] @@ -72,8 +80,8 @@ if version_info.major == 2 and version_info.minor < 7: - warnings.warn('Python2 version less than 7 is not supported', LeanCloudWarning) + warnings.warn("Python2 version less than 7 is not supported", LeanCloudWarning) if version_info.minor == 3 and version_info.minor < 4: - warnings.warn('Python3 version less than 4 is not supported', LeanCloudWarning) + warnings.warn("Python3 version less than 4 is not supported", LeanCloudWarning) diff --git a/leancloud/_compat.py b/leancloud/_compat.py deleted file mode 100644 index fd96f1b5..00000000 --- a/leancloud/_compat.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask._compat - ~~~~~~~~~~~~~ - - Some py2/py3 compatibility support based on a stripped down - version of six so we don't have to depend on a specific version - of it. - - :copyright: (c) 2015 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" - -from __future__ import absolute_import - -import sys -import types -import io - - -# Check the python version. -_version = sys.version_info.major -PY2 = _version == 2 -PY3 = _version == 3 - -_identity = lambda x: x - -if PY2: - text_type = unicode - class_types = (type, types.ClassType) - string_types = (str, unicode) - integer_types = (int, long) - range_type = xrange - file_type = file - buffer_type = buffer # python 2.6 don't have memoryview. - - iterkeys = lambda d: d.iterkeys() - itervalues = lambda d: d.itervalues() - iteritems = lambda d: d.iteritems() - - from StringIO import StringIO - BytesIO = StringIO - - exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') - - def implements_to_string(cls): - cls.__unicode__ = cls.__str__ - cls.__str__ = lambda x: x.__unicode__().encode('utf-8') - return cls - - def to_bytes(x, charset=sys.getdefaultencoding(), errors='strict'): - if x is None: - return None - if isinstance(x, (bytes, bytearray, buffer)): - return bytes(x) - if isinstance(x, unicode): - return x.encode(charset, errors) - raise TypeError('Expected bytes') - - def to_native(x, charset=sys.getdefaultencoding(), errors='strict'): - if x is None or isinstance(x, str): - return x - return x.encode(charset, errors) -elif PY3: - text_type = str - class_types = (type,) - string_types = (str,) - integer_types = (int,) - range_type = range - file_type = io.IOBase - buffer_type = memoryview - - iterkeys = lambda d: iter(d.keys()) - itervalues = lambda d: iter(d.values()) - iteritems = lambda d: iter(d.items()) - - from io import BytesIO - - def reraise(tp, value, tb=None): - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - - implements_to_string = _identity - - def to_bytes(x, charset=sys.getdefaultencoding(), errors='strict'): - if x is None: - return None - if isinstance(x, (bytes, bytearray, memoryview)): - return bytes(x) - if isinstance(x, str): - return x.encode(charset, errors) - raise TypeError('Expected bytes') - - def to_native(x, charset=sys.getdefaultencoding(), errors='strict'): - if x is None or isinstance(x, str): - return x - return x.decode(charset, errors) - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a - # dummy metaclass for one level of class instantiation that replaces - # itself with the actual metaclass. - class metaclass(type): - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) diff --git a/leancloud/acl.py b/leancloud/acl.py index 9279dc70..91ab8085 100644 --- a/leancloud/acl.py +++ b/leancloud/acl.py @@ -3,14 +3,16 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals + +import six import leancloud -from leancloud._compat import string_types -__author__ = 'asaka ' +__author__ = "asaka " -PUBLIC_KEY = '*' +PUBLIC_KEY = "*" class ACL(object): @@ -24,7 +26,7 @@ def _set_access(self, access_type, user_id, allowed): if isinstance(user_id, leancloud.User): user_id = user_id.id elif isinstance(user_id, leancloud.Role): - user_id = 'role:' + user_id.get_name() + user_id = "role:" + user_id.get_name() permissions = self.permissions_by_id.get(user_id) if permissions is None: if not allowed: @@ -43,23 +45,23 @@ def _get_access(self, access_type, user_id): if isinstance(user_id, leancloud.User): user_id = user_id.id elif isinstance(user_id, leancloud.Role): - user_id = 'role:' + user_id.get_name() + user_id = "role:" + user_id.get_name() permissions = self.permissions_by_id.get(user_id) if not permissions: return False return permissions.get(access_type, False) def set_read_access(self, user_id, allowed): - return self._set_access('read', user_id, allowed) + return self._set_access("read", user_id, allowed) def get_read_access(self, user_id): - return self._get_access('read', user_id) + return self._get_access("read", user_id) def set_write_access(self, user_id, allowed): - return self._set_access('write', user_id, allowed) + return self._set_access("write", user_id, allowed) def get_write_access(self, user_id): - return self._get_access('write', user_id) + return self._get_access("write", user_id) def set_public_read_access(self, allowed): return self.set_read_access(PUBLIC_KEY, allowed) @@ -76,27 +78,27 @@ def get_public_write_access(self): def set_role_read_access(self, role, allowed): if isinstance(role, leancloud.Role): role = role.get_name() - if not isinstance(role, string_types): - raise TypeError('role must be a leancloud.Role or str') - self.set_read_access('role:{0}'.format(role), allowed) + if not isinstance(role, six.string_types): + raise TypeError("role must be a leancloud.Role or str") + self.set_read_access("role:{0}".format(role), allowed) def get_role_read_access(self, role): if isinstance(role, leancloud.Role): role = role.get_name() - if not isinstance(role, string_types): - raise TypeError('role must be a leancloud.Role or str') - return self.get_read_access('role:{0}'.format(role)) + if not isinstance(role, six.string_types): + raise TypeError("role must be a leancloud.Role or str") + return self.get_read_access("role:{0}".format(role)) def set_role_write_access(self, role, allowed): if isinstance(role, leancloud.Role): role = role.get_name() - if not isinstance(role, string_types): - raise TypeError('role must be a leancloud.Role or str') - self.set_write_access('role:{0}'.format(role), allowed) + if not isinstance(role, six.string_types): + raise TypeError("role must be a leancloud.Role or str") + self.set_write_access("role:{0}".format(role), allowed) def get_role_write_access(self, role): if isinstance(role, leancloud.Role): role = role.get_name() - if not isinstance(role, string_types): - raise TypeError('role must be a leancloud.Role or str') - return self.get_write_access('role:{0}'.format(role)) + if not isinstance(role, six.string_types): + raise TypeError("role must be a leancloud.Role or str") + return self.get_write_access("role:{0}".format(role)) diff --git a/leancloud/app_router.py b/leancloud/app_router.py index c0f3aa2f..5b2207b1 100644 --- a/leancloud/app_router.py +++ b/leancloud/app_router.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import sys import time @@ -12,28 +13,58 @@ class AppRouter(object): - def __init__(self, app_id): - self.lock = threading.Lock() + def __init__(self, app_id, region): self.app_id = app_id - self.api_server = None + self.region = region + self.hosts = {} + self.session = requests.Session() + self.lock = threading.Lock() self.expired_at = 0 - def get(self): - if self.api_server is not None and self.expired_at > time.time(): - return self.api_server + prefix = app_id[:8].lower() + is_cn_n1 = False + + if region == "US": + domain = "lncldglobal.com" + elif region == "CN": + if app_id.endswith("-9Nh9j0Va"): + domain = "lncldapi.com" + elif app_id.endswith("-MdYXbMMI"): + domain = "lncldglobal.com" + else: + domain = "{}.lc-cn-n1-shared.com".format(prefix) + is_cn_n1 = True else: - with self.lock: - return self.refresh() + raise RuntimeError("invalid region: {}".format(region)) + + if is_cn_n1: + self.hosts.update(dict.fromkeys( + ["api", "engine", "stats", "push"], domain)) + else: + self.hosts["api"] = "{}.api.{}".format(prefix, domain) + self.hosts["engine"] = "{}.engine.{}".format(prefix, domain) + self.hosts["stats"] = "{}.stats.{}".format(prefix, domain) + self.hosts["push"] = "{}.push.{}".format(prefix, domain) + + def get(self, type_): + with self.lock: + if time.time() > self.expired_at: + self.expired_at += 600 + threading.Thread(target=self.refresh).start() + return self.hosts[type_] def refresh(self): - url = 'https://app-router.leancloud.cn/1/route?appId={}'.format(self.app_id) + url = "https://app-router.com/2/route?appId={}".format(self.app_id) try: - result = requests.get(url).json() - self.update(result) - return result['api_server'] + result = self.session.get(url, timeout=5).json() + with self.lock: + self.update(result) except Exception as e: - print('refresh app router failed: ', e, file=sys.stderr) + print("refresh app router failed:", e, file=sys.stderr) def update(self, content): - self.api_server = content['api_server'] - self.expired_at = time.time() + content['ttl'] + self.hosts["api"] = content["api_server"] + self.hosts["engine"] = content["engine_server"] + self.hosts["stats"] = content["stats_server"] + self.hosts["push"] = content["push_server"] + self.expired_at = time.time() + content["ttl"] diff --git a/leancloud/client.py b/leancloud/client.py index 137b5dd5..fe9ba1bc 100644 --- a/leancloud/client.py +++ b/leancloud/client.py @@ -3,45 +3,57 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import os import json import time import hashlib +import functools + +import six import requests +from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter import leancloud from leancloud import utils -from leancloud._compat import iteritems -from leancloud._compat import to_bytes from leancloud.app_router import AppRouter -__author__ = 'asaka ' +__author__ = "asaka " APP_ID = None APP_KEY = None MASTER_KEY = None -USE_PRODUCTION = '1' +HOOK_KEY = None +if os.getenv("LEANCLOUD_APP_ENV") == "production": + USE_PRODUCTION = "1" +elif os.getenv("LEANCLOUD_APP_ENV") == "stage": + USE_PRODUCTION = "0" +else: # probably on local machine + if os.getenv("LEAN_CLI_HAVE_STAGING") == "true": + USE_PRODUCTION = "0" + else: # free trial instance only + USE_PRODUCTION = "1" + USE_HTTPS = True # 兼容老版本,如果 USE_MASTER_KEY 为 None ,并且 MASTER_KEY 不为 None,则使用 MASTER_KEY # 否则依据 USE_MASTER_KEY 来决定是否使用 MASTER_KEY USE_MASTER_KEY = None -REGION = 'CN' +REGION = "CN" app_router = None +session = requests.Session() +session.mount("http://", TCPKeepAliveAdapter()) +session.mount("https://", TCPKeepAliveAdapter()) +request_hooks = {} -SERVER_URLS = { - 'CN': 'api.leancloud.cn', - 'US': 'us-api.leancloud.cn', -} - -SERVER_VERSION = '1.1' +SERVER_VERSION = "1.1" TIMEOUT_SECONDS = 15 -def init(app_id, app_key=None, master_key=None): +def init(app_id, app_key=None, master_key=None, hook_key=None): """初始化 LeanCloud 的 AppId / AppKey / MasterKey :type app_id: string_types @@ -50,68 +62,84 @@ def init(app_id, app_key=None, master_key=None): :param app_key: 应用的 Application Key :type master_key: None or string_types :param master_key: 应用的 Master Key + :param hook_key: application's hook key + :type hook_key: None or string_type """ if (not app_key) and (not master_key): - raise RuntimeError('app_key or master_key must be specified') - global APP_ID, APP_KEY, MASTER_KEY + raise RuntimeError("app_key or master_key must be specified") + global APP_ID, APP_KEY, MASTER_KEY, HOOK_KEY APP_ID = app_id APP_KEY = app_key MASTER_KEY = master_key + if hook_key: + HOOK_KEY = hook_key + else: + HOOK_KEY = os.environ.get("LEANCLOUD_APP_HOOK_KEY") def need_init(func): + @functools.wraps(func) def new_func(*args, **kwargs): if APP_ID is None: - raise RuntimeError('LeanCloud SDK must be initialized') + raise RuntimeError("LeanCloud SDK must be initialized") headers = { - 'Content-Type': 'application/json;charset=utf-8', - 'X-AVOSCloud-Application-Id': APP_ID, - 'X-AVOSCloud-Application-Production': USE_PRODUCTION, - 'User-Agent': 'AVOS Cloud python-{0} SDK'.format(leancloud.__version__), + "Content-Type": "application/json;charset=utf-8", + "X-LC-Id": APP_ID, + "X-LC-Hook-Key": HOOK_KEY, + "X-LC-Prod": USE_PRODUCTION, + "User-Agent": "AVOS Cloud python-{0} SDK ({1}.{2})".format( + leancloud.__version__, + leancloud.version_info.major, + leancloud.version_info.minor, + ), } md5sum = hashlib.md5() - current_time = str(int(time.time() * 1000)) + current_time = six.text_type(int(time.time() * 1000)) if (USE_MASTER_KEY is None and MASTER_KEY) or USE_MASTER_KEY is True: - # md5sum.update(current_time + MASTER_KEY) - # headers['X-AVOSCloud-Request-Sign'] = md5sum.hexdigest() + ',' + current_time + ',master' - headers['X-AVOSCloud-Master-Key'] = MASTER_KEY + md5sum.update((current_time + MASTER_KEY).encode("utf-8")) + headers["X-LC-Sign"] = md5sum.hexdigest() + "," + current_time + ",master" else: # In python 2.x, you can feed this object with arbitrary # strings using the update() method, but in python 3.x, # you should feed with bytes-like objects. - md5sum.update(to_bytes(current_time + APP_KEY)) - headers['X-AVOSCloud-Request-Sign'] = md5sum.hexdigest() + ',' + current_time + md5sum.update((current_time + APP_KEY).encode("utf-8")) + headers["X-LC-Sign"] = md5sum.hexdigest() + "," + current_time user = leancloud.User.get_current() if user: - headers['X-AVOSCloud-Session-Token'] = user._session_token + headers["X-LC-Session"] = user._session_token return func(headers=headers, *args, **kwargs) + return new_func -def get_base_url(): +def get_url(part): # try to use the base URL from environ - url = os.environ.get('LC_API_SERVER') or os.environ.get('LEANCLOUD_API_SERVER') + url = os.environ.get("LEANCLOUD_API_SERVER") or os.environ.get("LC_API_SERVER") if url: - return '{}/{}'.format(url, SERVER_VERSION) - - if REGION == 'US': - # use the hard coded base URL if region is US - host = SERVER_URLS[REGION] + return "{}/{}{}".format(url, SERVER_VERSION, part) + + global app_router + if app_router is None: + app_router = AppRouter(APP_ID, REGION) + + if part.startswith("/push") or part.startswith("/installations"): + host = app_router.get("push") + elif part.startswith("/collect"): + host = app_router.get("stats") + elif part.startswith("/functions") or part.startswith("/call"): + host = app_router.get("engine") else: - # use base URL from app router - global app_router - if app_router is None: - app_router = AppRouter(APP_ID) - host = app_router.get() + host = app_router.get("api") r = { - 'schema': 'https' if USE_HTTPS else 'http', - 'version': SERVER_VERSION, - 'host': host, + "schema": "https" if USE_HTTPS else "http", + "version": SERVER_VERSION, + "host": host, + "part": part, } - return '{schema}://{host}/{version}'.format(**r) + return "{schema}://{host}/{version}{part}".format(**r) def use_production(flag): @@ -119,7 +147,7 @@ def use_production(flag): 默认调用生产环境。 """ global USE_PRODUCTION - USE_PRODUCTION = '1' if flag else '0' + USE_PRODUCTION = "1" if flag else "0" def use_master_key(flag=True): @@ -133,108 +161,105 @@ def use_master_key(flag=True): USE_MASTER_KEY = False return if not MASTER_KEY: - raise RuntimeError('LeanCloud SDK master key not specified') + raise RuntimeError("LeanCloud SDK master key not specified") USE_MASTER_KEY = True -# def use_https(flag=True): -# """是否启用 HTTPS 和 LeanCloud 存储服务器通讯。 -# 默认启用,在 LeanEngine 环境下关闭可以大幅提高 LeanCloud 存储服务查询性能。 -# -# :type flag: bool -# """ -# global USE_HTTPS -# if not flag: -# USE_HTTPS = False -# else: -# USE_HTTPS = True - - def check_error(func): + @functools.wraps(func) def new_func(*args, **kwargs): response = func(*args, **kwargs) assert isinstance(response, requests.Response) - if response.headers.get('Content-Type') == 'text/html': - raise leancloud.LeanCloudError(-1, 'Bad Request') + if response.headers.get("Content-Type") == "text/html": + raise leancloud.LeanCloudError(-1, "Bad Request") content = response.json() - if 'error' in content: - raise leancloud.LeanCloudError(content.get('code', 1), content.get('error', 'Unknown Error')) - - return response - return new_func + if "error" in content: + raise leancloud.LeanCloudError( + content.get("code", 1), content.get("error", "Unknown Error") + ) - -def region_redirect(func): - def new_func(*args, **kwargs): - response = func(*args, **kwargs) - if response.status_code == 307: - # we are requests another region's API server, and it told us to request another API server - content = response.json() - if app_router: - app_router.update(content) - response = func(*args, **kwargs) return response return new_func def use_region(region): - if region not in SERVER_URLS: - raise ValueError('currently no nodes in the region') + if region not in ("CN", "US"): + raise ValueError("currently no nodes in the region") global REGION REGION = region def get_server_time(): - response = requests.get(get_base_url() + '/date') - content = json.loads(response.text) - return utils.decode('iso', content) + response = check_error(session.get)(get_url("/date"), timeout=TIMEOUT_SECONDS) + return utils.decode("iso", response.json()) def get_app_info(): return { - 'app_id': APP_ID, - 'app_key': APP_KEY, - 'master_key': MASTER_KEY, + "app_id": APP_ID, + "app_key": APP_KEY, + "master_key": MASTER_KEY, + "hook_key": HOOK_KEY, } @need_init -@region_redirect @check_error def get(url, params=None, headers=None): if not params: params = {} else: - for k, v in iteritems(params): + for k, v in six.iteritems(params): if isinstance(v, dict): - params[k] = json.dumps(v, separators=(',', ':')) - response = requests.get(get_base_url() + url, headers=headers, params=params, timeout=TIMEOUT_SECONDS) + params[k] = json.dumps(v, separators=(",", ":")) + response = session.get( + get_url(url), + headers=headers, + params=params, + timeout=TIMEOUT_SECONDS, + hooks=request_hooks, + ) return response @need_init -@region_redirect @check_error def post(url, params, headers=None): - response = requests.post(get_base_url() + url, headers=headers, data=json.dumps(params, separators=(',', ':')), timeout=TIMEOUT_SECONDS) + response = session.post( + get_url(url), + headers=headers, + data=json.dumps(params, separators=(",", ":")), + timeout=TIMEOUT_SECONDS, + hooks=request_hooks, + ) return response @need_init -@region_redirect @check_error def put(url, params, headers=None): - response = requests.put(get_base_url() + url, headers=headers, data=json.dumps(params, separators=(',', ':')), timeout=TIMEOUT_SECONDS) + response = session.put( + get_url(url), + headers=headers, + data=json.dumps(params, separators=(",", ":")), + timeout=TIMEOUT_SECONDS, + hooks=request_hooks, + ) return response @need_init -@region_redirect @check_error def delete(url, params=None, headers=None): - response = requests.delete(get_base_url() + url, headers=headers, data=json.dumps(params, separators=(',', ':')), timeout=TIMEOUT_SECONDS) + response = session.delete( + get_url(url), + headers=headers, + data=json.dumps(params, separators=(",", ":")), + timeout=TIMEOUT_SECONDS, + hooks=request_hooks, + ) return response diff --git a/leancloud/cloud.py b/leancloud/cloud.py new file mode 100644 index 00000000..d9c546a8 --- /dev/null +++ b/leancloud/cloud.py @@ -0,0 +1,189 @@ +# coding: utf-8 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import six + +import leancloud +from leancloud import utils +from leancloud.engine import leanengine + + +__author__ = "asaka " + + +def run(_cloud_func_name, **params): + """ + 调用 LeanEngine 上的远程代码 + :param name: 需要调用的远程 Cloud Code 的名称 + :type name: string_types + :param params: 调用参数 + :return: 调用结果 + """ + response = leancloud.client.post( + "/functions/{0}".format(_cloud_func_name), params=params + ) + content = response.json() + return utils.decode(None, content)["result"] + + +def _run_in_local(_cloud_func_name, **params): + if not leanengine.root_engine: + return + result = leanengine.dispatch_cloud_func( + leanengine.root_engine.app.cloud_codes, {}, _cloud_func_name, False, params + ) + return utils.decode(None, result) + + +run.remote = run +run.local = _run_in_local + + +def rpc(_cloud_rpc_name, **params): + """ + 调用 LeanEngine 上的远程代码 + 与 cloud.run 类似,但是允许传入 leancloud.Object 作为参数,也允许传入 leancloud.Object 作为结果 + :param name: 需要调用的远程 Cloud Code 的名称 + :type name: basestring + :param params: 调用参数 + :return: 调用结果 + """ + encoded_params = {} + for key, value in params.items(): + if isinstance(params, leancloud.Object): + encoded_params[key] = utils.encode(value._dump()) + else: + encoded_params[key] = utils.encode(value) + response = leancloud.client.post( + "/call/{}".format(_cloud_rpc_name), params=encoded_params + ) + content = response.json() + return utils.decode(None, content["result"]) + + +def _rpc_in_local(_cloud_rpc_name, **params): + if not leanengine.root_engine: + return + result = leanengine.dispatch_cloud_func( + leanengine.root_engine.app.cloud_codes, {}, _cloud_rpc_name, True, params + ) + return utils.decode(None, result) + + +rpc.remote = rpc +rpc.local = _rpc_in_local + + +def request_sms_code( + phone_number, + idd="+86", + sms_type="sms", + validate_token=None, + template=None, + sign=None, + params=None, +): + """ + 请求发送手机验证码 + :param phone_number: 需要验证的手机号码 + :param idd: 号码的所在地国家代码,默认为中国(+86) + :param sms_type: 验证码发送方式,'voice' 为语音,'sms' 为短信 + :param template: 模版名称 + :param sign: 短信签名名称 + :return: None + """ + if not isinstance(phone_number, six.string_types): + raise TypeError("phone_number must be a string") + + data = { + "mobilePhoneNumber": phone_number + if phone_number.startswith("+") + else idd + phone_number, + "smsType": sms_type, + } + + if template is not None: + data["template"] = template + + if sign is not None: + data["sign"] = sign + + if validate_token is not None: + data["validate_token"] = validate_token + + if params is not None: + data.update(params) + + leancloud.client.post("/requestSmsCode", params=data) + + +def verify_sms_code(phone_number, code): + """ + 获取到手机验证码之后,验证验证码是否正确。如果验证失败,抛出异常。 + :param phone_number: 需要验证的手机号码 + :param code: 接受到的验证码 + :return: None + """ + params = { + "mobilePhoneNumber": phone_number, + } + leancloud.client.post("/verifySmsCode/{0}".format(code), params=params) + return True + + +class Captcha(object): + """ + 表示图形验证码 + """ + + def __init__(self, token, url): + self.token = token + self.url = url + + def verify(self, code): + """ + 验证用户输入与图形验证码是否匹配 + :params code: 用户填写的验证码 + """ + return verify_captcha(code, self.token) + + +def request_captcha(size=None, width=None, height=None, ttl=None): + """ + 请求生成新的图形验证码 + :return: Captcha + """ + params = { + "size": size, + "width": width, + "height": height, + "ttl": ttl, + } + params = {k: v for k, v in params.items() if v is not None} + + response = leancloud.client.get("/requestCaptcha", params) + content = response.json() + return Captcha(content["captcha_token"], content["captcha_url"]) + + +def verify_captcha(code, token): + """ + 验证用户输入与图形验证码是否匹配 + :params code: 用户填写的验证码 + :params token: 图形验证码对应的 token + :return: validate token + """ + params = { + "captcha_token": token, + "captcha_code": code, + } + response = leancloud.client.post("/verifyCaptcha", params) + return response.json()["validate_token"] + + +def get_server_time(): + return leancloud.client.get_server_time() diff --git a/leancloud/conversation.py b/leancloud/conversation.py new file mode 100644 index 00000000..fd83e35b --- /dev/null +++ b/leancloud/conversation.py @@ -0,0 +1,152 @@ +# coding: utf-8 + +""" +实时通讯会话相关操作。 +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import json +from datetime import datetime + +import arrow + +from leancloud import client +from leancloud.object_ import Object + + +class Conversation(Object): + """ + :param name: 会话名称 + :param is_system: 是否系统会话 + :param is_transient: 是否暂态会话 + :param is_unique: 是否重用成员相同的会话(暂停会话不支持此参数) + """ + + def __init__(self, name=None, is_system=False, is_transient=False, is_unique=None): + super(Conversation, self).__init__() + if name: + self.set("name", name) + if is_system: + self.set("sys", True) + + if is_transient: + self.set("tr", True) + else: + if is_unique is not None: + self.set("unique", is_unique) + + @property + def name(self): + """ + 获取此会话名称。 + """ + return self.get("name") + + @property + def creator(self): + """ + 获取此会话创建者。 + """ + return self.get("c") + + @property + def last_message_read_at(self): + """ + 获取此会话最后一条已读消息时间。 + """ + return self.get("lm") + + @property + def members(self): + """ + 获取此会话所有参与者。 + """ + return self.get("m") + + @property + def muted_members(self): + """ + 获取所有将此会话设置为静音的参与者。 + """ + return self.get("mu") + + @property + def is_system(self): + """ + 是否为系统会话。 + """ + return self.get("sys") + + @property + def is_transient(self): + """ + 是否为暂态会话。 + """ + return self.get("tr") + + @property + def is_unique(self): + """ + 是否为 unique 会话。 + """ + return self.get("unique") + + def add_member(self, client_id): + """ + 将指定参与者加入会话。 + """ + return self.add("m", client_id) + + def send( + self, from_client, message, to_clients=None, transient=False, push_data=None + ): + """ + 在指定会话中发送消息。 + + :param from_client: 发送者 id + :param message: 消息内容 + :param to_clients: 接受者 id,只在系统会话中生效 + :param transient: 是否以暂态形式发送消息 + :param push_data: 推送消息内容,参考:https://url.leanapp.cn/pushData + """ + if isinstance(message, dict): + message = json.dumps(message) + params = { + "from_peer": from_client, + "conv_id": self.id, + "transient": transient, + "message": message, + } + if to_clients: + params["to_peers"] = to_clients + if push_data: + params["push_data"] = push_data + client.post("/rtm/messages", params=params).json() + + def broadcast(self, from_client, message, valid_till=None, push_data=None): + """ + 发送广播消息,只能以系统会话名义发送。全部用户都会收到此消息,不论是否在此会话中。 + + :param from_client: 发送者 id + :param message: 消息内容 + :param valid_till: 指定广播消息过期时间 + :param puhs_data: 推送消息内容,参考:https://url.leanapp.cn/pushData + """ + if isinstance(message, dict): + message = json.dumps(message) + params = { + "from_peer": from_client, + "conv_id": self.id, + "message": message, + } + if push_data: + params["push_data"] = push_data + if valid_till: + if isinstance(valid_till, datetime): + valid_till = arrow.get(valid_till).datetime + params["valid_till"] = valid_till + client.post("/rtm/broadcast", params=params).json() diff --git a/leancloud/engine/__init__.py b/leancloud/engine/__init__.py index ad60a5b6..076d50f0 100644 --- a/leancloud/engine/__init__.py +++ b/leancloud/engine/__init__.py @@ -1,74 +1,192 @@ # coding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import os import sys import json +import warnings from werkzeug.wrappers import Request from werkzeug.wrappers import Response from werkzeug.serving import run_simple import leancloud -from . import context +from . import utils +from . import leanengine from .authorization import AuthorizationMiddleware +from .cookie_session import CookieSessionMiddleware # noqa: F401 from .cors import CORSMiddleware +from .https_redirect_middleware import HttpsRedirectMiddleware # noqa: F401 from .leanengine import LeanEngineApplication from .leanengine import LeanEngineError -from .leanengine import register_cloud_func -from .leanengine import register_on_verified -from .leanengine import register_on_login -from .leanengine import before_save +from .leanengine import after_delete from .leanengine import after_save -from .leanengine import before_update from .leanengine import after_update from .leanengine import before_delete -from .leanengine import after_delete -from .leanengine import user +from .leanengine import before_save +from .leanengine import before_update +from .leanengine import context +from .leanengine import current +from .leanengine import register_cloud_func from .leanengine import register_on_bigquery +from .leanengine import register_on_login +from .leanengine import register_on_auth_data +from .leanengine import register_on_verified +from .leanengine import user -__author__ = 'asaka ' +__author__ = "asaka " class Engine(object): - def __init__(self, wsgi_app): - self.current_user = user + """ + LeanEngine middleware. + """ + + def __init__(self, wsgi_app=None, fetch_user=True): + """ + LeanEngine middleware constructor. + + :param wsgi_app: wsgi callable + :param fetch_user: + should fetch user's data from server while prNoneocessing session token. + :type fetch_user: bool + """ + self.current = current + if wsgi_app: + leanengine.root_engine = self self.origin_app = wsgi_app - self.cloud_app = context.local_manager.make_middleware(CORSMiddleware(AuthorizationMiddleware(LeanEngineApplication()))) + self.app = LeanEngineApplication(fetch_user=fetch_user) + self.cloud_app = context.local_manager.make_middleware( + CORSMiddleware(AuthorizationMiddleware(self.app)) + ) def __call__(self, environ, start_response): request = Request(environ) - environ['leanengine.request'] = request # cache werkzeug request for other middlewares + environ[ + "leanengine.request" + ] = request # cache werkzeug request for other middlewares - if request.path in ('/__engine/1/ping', '/__engine/1.1/ping/'): - start_response('200 OK', [('Content-Type', 'application/json')]) + if request.path in ("/__engine/1/ping", "/__engine/1.1/ping/"): + start_response( + utils.to_native("200 OK"), + [ + ( + utils.to_native("Content-Type"), + utils.to_native("application/json"), + ) + ], + ) version = sys.version_info - return Response(json.dumps({ - 'version': leancloud.__version__, - 'runtime': 'cpython-{0}.{1}.{2}'.format(version.major, version.minor, version.micro) - }))(environ, start_response) - if request.path.startswith('/__engine/'): + return Response( + json.dumps( + { + "version": leancloud.__version__, + "runtime": "cpython-{0}.{1}.{2}".format( + version.major, version.minor, version.micro + ), + } + ) + )(environ, start_response) + if request.path.startswith("/__engine/"): return self.cloud_app(environ, start_response) - if request.path.startswith('/1/functions') or request.path.startswith('/1.1/functions'): + if request.path.startswith("/1/functions") or request.path.startswith( + "/1.1/functions" + ): return self.cloud_app(environ, start_response) - if request.path.startswith('/1/call') or request.path.startswith('/1.1/call'): + if request.path.startswith("/1/call") or request.path.startswith("/1.1/call"): return self.cloud_app(environ, start_response) return self.origin_app(environ, start_response) - define = staticmethod(register_cloud_func) - on_verified = staticmethod(register_on_verified) - on_login = staticmethod(register_on_login) - before_save = staticmethod(before_save) - after_save = staticmethod(after_save) - before_update = staticmethod(before_update) - after_update = staticmethod(after_update) - before_delete = staticmethod(before_delete) - after_delete = staticmethod(after_delete) - on_bigquery = staticmethod(register_on_bigquery) - - run = staticmethod(run_simple) - - -__all__ = [ - 'user', - 'Engine', - 'LeanEngineError' -] + def wrap(self, wsgi_app): + if leanengine.root_engine: + raise RuntimeError("It's forbidden that overwriting wsgi_func.") + leanengine.root_engine = self + self.origin_app = wsgi_app + return self + + def register(self, engine): + if not isinstance(engine, Engine): + raise TypeError("Please specify an Engine instance") + self.app.update_cloud_codes(engine.app.cloud_codes) + + def define(self, *args, **kwargs): + return register_cloud_func(self.app.cloud_codes, *args, **kwargs) + + def on_verified(self, *args, **kwargs): + return register_on_verified(self.app.cloud_codes, *args, **kwargs) + + def on_login(self, *args, **kwargs): + return register_on_login(self.app.cloud_codes, *args, **kwargs) + + def on_auth_data(self, *args, **kwargs): + return register_on_auth_data(self.app.cloud_codes, *args, **kwargs) + + def on_bigquery(self, *args, **kwargs): + warnings.warn( + "on_bigquery is deprecated, please use on_insight instead", + leancloud.LeanCloudWarning, + ) + return register_on_bigquery(self.app.cloud_codes, *args, **kwargs) + + def before_save(self, *args, **kwargs): + return before_save(self.app.cloud_codes, *args, **kwargs) + + def after_save(self, *args, **kwargs): + return after_save(self.app.cloud_codes, *args, **kwargs) + + def before_update(self, *args, **kwargs): + return before_update(self.app.cloud_codes, *args, **kwargs) + + def after_update(self, *args, **kwargs): + return after_update(self.app.cloud_codes, *args, **kwargs) + + def before_delete(self, *args, **kwargs): + return before_delete(self.app.cloud_codes, *args, **kwargs) + + def after_delete(self, *args, **kwargs): + return after_delete(self.app.cloud_codes, *args, **kwargs) + + def on_insight(self, *args, **kwargs): + return register_on_bigquery(self.app.cloud_codes, *args, **kwargs) + + def run(self, *args, **kwargs): + return run_simple(*args, **kwargs) + + def start(self): + from gevent.pywsgi import WSGIServer + + if not hasattr(leancloud, "APP_ID"): + APP_ID = os.environ["LEANCLOUD_APP_ID"] + APP_KEY = os.environ["LEANCLOUD_APP_KEY"] + MASTER_KEY = os.environ["LEANCLOUD_APP_MASTER_KEY"] + HOOK_KEY = os.environ["LEANCLOUD_APP_HOOK_KEY"] + PORT = int(os.environ.get("LEANCLOUD_APP_PORT")) + leancloud.init( + APP_ID, app_key=APP_KEY, master_key=MASTER_KEY, hook_key=HOOK_KEY + ) + + def application(environ, start_response): + start_response( + "200 OK".encode("utf-8"), + [("Content-Type".encode("utf-8"), "text/plain".encode("utf-8"))], + ) + return "This is a LeanEngine application." + + class NopLogger(object): + def write(self, s): + pass + + app = self.wrap(application) + self.server = WSGIServer(("", PORT), app, log=NopLogger()) + print("LeanEngine Cloud Functions app is running, port:", PORT) + self.server.serve_forever() + + def stop(self): + self.server.stop() + + +__all__ = ["user", "Engine", "LeanEngineError"] diff --git a/leancloud/engine/authorization.py b/leancloud/engine/authorization.py index 43498a22..759d749f 100644 --- a/leancloud/engine/authorization.py +++ b/leancloud/engine/authorization.py @@ -1,19 +1,25 @@ # coding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + import os import json from werkzeug.wrappers import Response from . import utils -from leancloud._compat import to_native -__author__ = 'asaka ' +__author__ = "asaka " -APP_ID = os.environ.get('LC_APP_ID') -APP_KEY = os.environ.get('LC_APP_KEY') -MASTER_KEY = os.environ.get('LC_APP_MASTER_KEY') +APP_ID = os.environ.get("LEANCLOUD_APP_ID") +APP_KEY = os.environ.get("LEANCLOUD_APP_KEY") +ANDX_KEY = os.environ.get("LEANCLOUD_APP_ANDX_KEY") +MASTER_KEY = os.environ.get("LEANCLOUD_APP_MASTER_KEY") +HOOK_KEY = os.environ.get("LEANCLOUD_APP_HOOK_KEY") _ENABLE_TEST = False @@ -30,76 +36,97 @@ def __call__(self, environ, start_response): current_environ = environ self.parse_header(environ) - app_params = environ['_app_params'] + app_params = environ["_app_params"] if not any(app_params.values()): # all app_params's value is None self.parse_body(environ) - unauth_response = Response(json.dumps({ - 'code': 401, 'error': 'Unauthorized.' - }), status=401, mimetype='application/json') - if app_params['id'] is None: + unauth_response = Response( + json.dumps({"code": 401, "error": "Unauthorized."}), + status=401, + mimetype="application/json", + ) + if app_params["id"] is None: return unauth_response(environ, start_response) - if (APP_ID == app_params['id']) and (app_params['key'] in [MASTER_KEY, APP_KEY]): + if (APP_ID == app_params["id"]) and ( + app_params["key"] in [MASTER_KEY, APP_KEY, ANDX_KEY] + ): return self.app(environ, start_response) - if (APP_ID == app_params['id']) and (app_params['master_key'] == MASTER_KEY): + if (APP_ID == app_params["id"]) and (app_params["master_key"] == MASTER_KEY): return self.app(environ, start_response) return unauth_response(environ, start_response) @classmethod def parse_header(cls, environ): - request = environ['leanengine.request'] - - app_id = request.headers.get('x-avoscloud-application-id')\ - or request.headers.get('x-uluru-application-id')\ - or request.headers.get('x-lc-id') - app_key = request.headers.get('x-avoscloud-application-key')\ - or request.headers.get('x-uluru-application-key')\ - or request.headers.get('x-lc-key') - session_token = request.headers.get('x-uluru-session-token')\ - or request.headers.get('x-avoscloud-session-token')\ - or request.headers.get('x-lc-session') - master_key = request.headers.get('x-uluru-master-key')\ - or request.headers.get('x-avoscloud-master-key') - - if app_key and ',master' in app_key: - master_key, _ = app_key.split(',') + request = environ["leanengine.request"] + + app_id = ( + request.headers.get("x-avoscloud-application-id") + or request.headers.get("x-uluru-application-id") + or request.headers.get("x-lc-id") + ) + app_key = ( + request.headers.get("x-avoscloud-application-key") + or request.headers.get("x-uluru-application-key") + or request.headers.get("x-lc-key") + ) + session_token = ( + request.headers.get("x-uluru-session-token") + or request.headers.get("x-avoscloud-session-token") + or request.headers.get("x-lc-session") + ) + master_key = request.headers.get("x-uluru-master-key") or request.headers.get( + "x-avoscloud-master-key" + ) + hook_key = request.headers.get("x-lc-hook-key") + + if app_key and ",master" in app_key: + master_key, _ = app_key.split(",") app_key = None if app_key is None: - request_sign = request.headers.get('x-avoscloud-request-sign')\ - or request.headers.get('x-lc-sign') + request_sign = request.headers.get( + "x-avoscloud-request-sign" + ) or request.headers.get("x-lc-sign") if request_sign: - request_sign = request_sign.split(',') if request_sign else [] + request_sign = request_sign.split(",") if request_sign else [] sign = request_sign[0].lower() timestamp = request_sign[1] - # key = MASTER_KEY if len(request_sign) == 3 and request_sign[2] == 'master' else APP_KEY + # if len(request_sign) == 3 and request_sign[2] == 'master': + # key = MASTER_KEY + # else: + # APP_KEY # if sign == utils.sign_by_key(timestamp, key): # app_key = key - if (len(request_sign) == 3)\ - and (request_sign[2] == 'master')\ - and (sign == utils.sign_by_key(timestamp, MASTER_KEY)): - master_key = MASTER_KEY + if len(request_sign) == 3: + if (request_sign[2] == "master") and ( + sign == utils.sign_by_key(timestamp, MASTER_KEY) + ): + master_key = MASTER_KEY + elif (request_sign[2] == "ax-sig-1") and ( + sign == utils.sign_by_key(timestamp, ANDX_KEY) + ): + app_key = ANDX_KEY elif sign == utils.sign_by_key(timestamp, APP_KEY): app_key = APP_KEY - environ['_app_params'] = { - 'id': app_id, - 'key': app_key, - 'master_key': master_key, - 'session_token': session_token, + environ["_app_params"] = { + "id": app_id, + "key": app_key, + "master_key": master_key, + "session_token": session_token, + "hook_key": hook_key, } @classmethod def parse_body(cls, environ): - request = environ['leanengine.request'] - if (not request.content_type) or ('text/plain' not in request.content_type): + request = environ["leanengine.request"] + if (not request.content_type) or ("text/plain" not in request.content_type): return - # the JSON object must be str, not 'bytes' for 3.x. - body = json.loads(to_native(request.data)) + body = json.loads(request.get_data(as_text=True)) - environ['_app_params']['id'] = body.get('_ApplicationId') - environ['_app_params']['key'] = body.get('_ApplicationKey') - environ['_app_params']['master_key'] = body.get('_MasterKey') - environ['_app_params']['session_token'] = body.get('_SessionToken') + environ["_app_params"]["id"] = body.get("_ApplicationId") + environ["_app_params"]["key"] = body.get("_ApplicationKey") + environ["_app_params"]["master_key"] = body.get("_MasterKey") + environ["_app_params"]["session_token"] = body.get("_SessionToken") diff --git a/leancloud/engine/cloudfunc.py b/leancloud/engine/cloudfunc.py deleted file mode 100644 index 18fc0e95..00000000 --- a/leancloud/engine/cloudfunc.py +++ /dev/null @@ -1,103 +0,0 @@ -# coding: utf-8 - -import leancloud -from leancloud import utils -from leancloud._compat import string_types - - -__author__ = 'asaka ' - - -def run(_cloud_func_name, **params): - """ - 调用 LeanEngine 上的远程代码 - - :param name: 需要调用的远程 Cloud Code 的名称 - :type name: string_types - :param params: 调用参数 - :return: 调用结果 - """ - response = leancloud.client.post('/functions/{0}'.format(_cloud_func_name), params=params) - content = response.json() - return utils.decode(None, content)['result'] - - -def _run_in_local(_cloud_func_name, **params): - result = leancloud.engine.leanengine.dispatch_cloud_func(_cloud_func_name, False, params) - return utils.decode(None, result) - - -run.remote = run -run.local = _run_in_local - - -def rpc(_cloud_rpc_name, **params): - """ - 调用 LeanEngine 上的远程代码 - 与cloudfunc.run 类似,但是允许传入 leancloud.Object 作为参数,也允许传入 leancloud.Object 作为结果 - - :param name: 需要调用的远程 Cloud Code 的名称 - :type name: basestring - :param params: 调用参数 - :return: 调用结果 - """ - encoded_params = {} - for key, value in params.items(): - if isinstance(params, leancloud.Object): - encoded_params[key] = utils.encode(value._dump()) - else: - encoded_params[key] = utils.encode(value) - response = leancloud.client.post('/call/{}'.format(_cloud_rpc_name), params=encoded_params) - content = response.json() - return utils.decode(None, content['result']) - - -def _rpc_in_local(_cloud_rpc_name, **params): - result = leancloud.engine.leanengine.dispatch_cloud_func(_cloud_rpc_name, True, params) - return utils.decode(None, result) - - -rpc.remote = rpc -rpc.local = _rpc_in_local - - -def request_sms_code(phone_number, idd='+86', sms_type='sms', template=None, params=None): - """ - 请求发送手机验证码 - - :param phone_number: 需要验证的手机号码 - :param idd: 号码的所在地国家代码,默认为中国(+86) - :param sms_type: 验证码发送方式,'voice' 为语音,'sms' 为短信 - :return: None - """ - if not isinstance(phone_number, string_types): - raise TypeError('phone_number must be a string') - - data = { - 'mobilePhoneNumber': phone_number, - 'smsType': sms_type, - 'IDD': idd, - } - - if template is not None: - params['template'] = template - - if params is not None: - data.update(params) - - leancloud.client.post('/requestSmsCode', params=data) - - -def verify_sms_code(phone_number, code): - """ - 获取到手机验证码之后,验证验证码是否正确。如果验证失败,抛出异常。 - - :param phone_number: 需要验证的手机号码 - :param code: 接受到的验证码 - :return: None - """ - params = { - 'mobilePhoneNumber': phone_number, - } - leancloud.client.post('/verifySmsCode/{0}'.format(code), params=params) - return True diff --git a/leancloud/engine/context.py b/leancloud/engine/context.py index a10e2c28..d7386f85 100644 --- a/leancloud/engine/context.py +++ b/leancloud/engine/context.py @@ -1,9 +1,24 @@ # coding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + from werkzeug.local import Local from werkzeug.local import LocalManager -__author__ = 'asaka ' +__author__ = "asaka " + + +class Current(object): + __slots__ = ["user", "session_token", "meta"] + + def __init__(self): + self.user = None + self.session_token = None + self.meta = None + local = Local() local_manager = LocalManager([local]) diff --git a/leancloud/engine/cookie_session.py b/leancloud/engine/cookie_session.py new file mode 100644 index 00000000..a32aaf41 --- /dev/null +++ b/leancloud/engine/cookie_session.py @@ -0,0 +1,121 @@ +# coding: utf-8 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from werkzeug import http +from werkzeug.wrappers import Request +from secure_cookie.cookie import SecureCookie + +from . import utils +from leancloud.user import User + + +__author__ = "asaka " + + +class CookieSessionMiddleware(object): + """ + 用来在 webhosting 功能中实现自动管理 LeanCloud 用户登录状态的 WSGI 中间件。 + 使用此中间件之后,在处理 web 请求中调用了 `leancloud.User.login()` 方法登录成功后, + 会将此用户 session token 写入到 cookie 中。 + 后续此次会话都可以通过 `leancloud.User.get_current()` 获取到此用户对象。 + + :param secret: 对保存在 cookie 中的用户 session token 进行签名时需要的 key,可使用任意方法随机生成,请不要泄漏 + :type secret: str + :param name: 在 cookie 中保存的 session token 的 key 的名称,默认为 "leancloud:session" + :type name: str + :param excluded_paths: + 指定哪些 URL path 不处理 session token,比如在处理静态文件的 URL path 上不进行处理,防止无谓的性能浪费 + :type excluded_paths: list + :param fetch_user: 处理请求时是否要从存储服务获取用户数据, + 如果为 false 的话, + leancloud.User.get_current() 获取到的用户数据上除了 session_token 之外没有任何其他数据, + 需要自己调用 fetch() 来获取。 + 为 true 的话,会自动在用户对象上调用 fetch(),这样将会产生一次数据存储的 API 调用。 + 默认为 false + :type fetch_user: bool + :param expires: 设置 cookie 的 expires + :type expires: int or datetime + :param max_age: 设置 cookie 的 max_age,单位为秒 + :type max_age: int + """ + + def __init__( + self, + app, + secret, + name="leancloud:session", + excluded_paths=None, + fetch_user=False, + expires=None, + max_age=None, + ): + if not secret: + raise RuntimeError("secret is required") + self.fetch_user = fetch_user + self.secret = secret + self.app = app + self.name = name + self.excluded_paths = [ + "/__engine/", + "/1/functions/", + "/1.1/functions/", + "/1/call/", + "/1.1/call/", + ] + self.expires = expires + self.max_age = max_age + if excluded_paths: + self.excluded_paths += excluded_paths + + def __call__(self, environ, start_response): + self.pre_process(environ) + + def new_start_response(status, response_headers, exc_info=None): + self.post_process(environ, response_headers) + return start_response(status, response_headers, exc_info) + + return self.app(environ, new_start_response) + + def pre_process(self, environ): + request = Request(environ) + for prefix in self.excluded_paths: + if request.path.startswith(prefix): + return + + cookie = request.cookies.get(self.name) + if not cookie: + return + + session = SecureCookie.unserialize(cookie, self.secret) + + if "session_token" not in session: + return + + if not self.fetch_user: + user = User() + user._session_token = session["session_token"] + user.id = session["uid"] + User.set_current(user) + else: + user = User.become(session["session_token"]) + User.set_current(user) + + def post_process(self, environ, headers): + user = User.get_current() + if not user: + cookies = http.parse_cookie(environ) + if self.name in cookies: + raw = http.dump_cookie(self.name, "", expires=1) + headers.append((utils.to_native("Set-Cookie"), raw)) + return + cookie = SecureCookie( + {"uid": user.id, "session_token": user.get_session_token()}, self.secret + ) + raw = http.dump_cookie( + self.name, cookie.serialize(), expires=self.expires, max_age=self.max_age + ) + headers.append((utils.to_native("Set-Cookie"), raw)) diff --git a/leancloud/engine/cors.py b/leancloud/engine/cors.py index 3c524403..c438024b 100644 --- a/leancloud/engine/cors.py +++ b/leancloud/engine/cors.py @@ -1,51 +1,91 @@ # coding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from . import utils + class CORSMiddleware(object): - ALLOW_ORIGIN = "*" - ALLOW_HEADERS = ', '.join([ - 'Content-Type', - 'X-AVOSCloud-Application-Id', - 'X-AVOSCloud-Application-Key', - 'X-AVOSCloud-Application-Production', - 'X-AVOSCloud-Client-Version', - 'X-AVOSCloud-Request-sign', - 'X-AVOSCloud-Session-Token', - 'X-AVOSCloud-Super-Key', - 'X-Requested-With', - 'X-Uluru-Application-Id,' - 'X-Uluru-Application-Key', - 'X-Uluru-Application-Production', - 'X-Uluru-Client-Version', - 'X-Uluru-Session-Token', - 'X-LC-Id', - 'X-LC-Key', - 'X-LC-Session', - 'X-LC-Sign', - 'X-LC-Prod', - 'X-LC-UA', - ]) - ALLOW_METHODS = ', '.join(['PUT', 'GET', 'POST', 'DELETE', 'OPTIONS']) - MAX_AGE = '86400' + ALLOW_ORIGIN = utils.to_native("*") + ALLOW_HEADERS = utils.to_native( + ", ".join( + [ + "Content-Type", + "X-AVOSCloud-Application-Id", + "X-AVOSCloud-Application-Key", + "X-AVOSCloud-Application-Production", + "X-AVOSCloud-Client-Version", + "X-AVOSCloud-Request-sign", + "X-AVOSCloud-Session-Token", + "X-AVOSCloud-Super-Key", + "X-Requested-With", + "X-Uluru-Application-Id," "X-Uluru-Application-Key", + "X-Uluru-Application-Production", + "X-Uluru-Client-Version", + "X-Uluru-Session-Token", + "X-LC-Hook-Key", + "X-LC-Id", + "X-LC-Key", + "X-LC-Prod", + "X-LC-Session", + "X-LC-Sign", + "X-LC-UA", + ] + ) + ) + ALLOW_METHODS = utils.to_native( + ", ".join(["PUT", "GET", "POST", "DELETE", "OPTIONS"]) + ) + MAX_AGE = utils.to_native("86400") def __init__(self, app): self.app = app def __call__(self, environ, start_response): - if environ['REQUEST_METHOD'] == 'OPTIONS': - start_response('200 OK', [ - ('Access-Control-Allow-Origin', environ.get('HTTP_ORIGIN', self.ALLOW_ORIGIN)), - ('Access-Control-Allow-Headers', self.ALLOW_HEADERS), - ('Access-Control-Allow-Methods', self.ALLOW_METHODS), - ('Access-Control-Max-Age', self.MAX_AGE) - ]) - return [''] + if environ["REQUEST_METHOD"] == "OPTIONS": + start_response( + utils.to_native("200 OK"), + [ + ( + utils.to_native("Access-Control-Allow-Origin"), + environ.get("HTTP_ORIGIN", self.ALLOW_ORIGIN), + ), + ( + utils.to_native("Access-Control-Allow-Headers"), + self.ALLOW_HEADERS, + ), + ( + utils.to_native("Access-Control-Allow-Methods"), + self.ALLOW_METHODS, + ), + (utils.to_native("Access-Control-Max-Age"), self.MAX_AGE), + ], + ) + return [utils.to_native("")] else: + def cors_start_response(status, headers, exc_info=None): - headers.append(('Access-Control-Allow-Origin', self.ALLOW_ORIGIN)) - headers.append(('Access-Control-Allow-Headers', self.ALLOW_HEADERS)) - headers.append(('Access-Control-Allow-Methods', self.ALLOW_METHODS)) - headers.append(('Access-Control-Max-Age', self.MAX_AGE)) + headers.append( + (utils.to_native("Access-Control-Allow-Origin"), self.ALLOW_ORIGIN) + ) + headers.append( + ( + utils.to_native("Access-Control-Allow-Headers"), + self.ALLOW_HEADERS, + ) + ) + headers.append( + ( + utils.to_native("Access-Control-Allow-Methods"), + self.ALLOW_METHODS, + ) + ) + headers.append( + (utils.to_native("Access-Control-Max-Age"), self.MAX_AGE) + ) return start_response(status, headers, exc_info) return self.app(environ, cors_start_response) diff --git a/leancloud/engine/https_redirect_middleware.py b/leancloud/engine/https_redirect_middleware.py index 228cdc26..bb587a2b 100644 --- a/leancloud/engine/https_redirect_middleware.py +++ b/leancloud/engine/https_redirect_middleware.py @@ -1,14 +1,20 @@ # coding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + import os + from werkzeug.wrappers import Request from werkzeug.utils import redirect -__author__ = 'asaka ' +__author__ = "asaka " -is_prod = True if os.environ.get('LC_APP_ENV') == 'production' else False +is_prod = True if os.environ.get("LEANCLOUD_APP_ENV") == "production" else False class HttpsRedirectMiddleware(object): @@ -17,10 +23,13 @@ def __init__(self, wsgi_app): def __call__(self, environ, start_response): request = Request(environ) - if is_prod and request.headers.get('X-Forwarded-Proto') != 'https': - url = 'https://{0}{1}'.format(request.host, request.path) - if request.query_string: - url += '?{0}'.format(request.query_string) + engine_health = "/1.1/functions/_ops/metadatas" + if ( + is_prod + and request.path != engine_health + and request.headers.get("X-Forwarded-Proto") != "https" + ): + url = "https://{0}{1}".format(request.host, request.full_path) return redirect(url)(environ, start_response) return self.origin_app(environ, start_response) diff --git a/leancloud/engine/leanengine.py b/leancloud/engine/leanengine.py index 8c5224db..30804832 100644 --- a/leancloud/engine/leanengine.py +++ b/leancloud/engine/leanengine.py @@ -1,198 +1,402 @@ # coding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import functools import json import logging +import sys import traceback -import functools -import leancloud -from werkzeug.wrappers import Response -from werkzeug.routing import Map -from werkzeug.routing import Rule +import six from werkzeug.exceptions import HTTPException from werkzeug.exceptions import NotAcceptable +from werkzeug.routing import Map +from werkzeug.routing import Rule +from werkzeug.wrappers import Response +import leancloud from . import context -from leancloud._compat import to_native - - -__author__ = 'asaka ' -logger = logging.getLogger('leancloud.cloudcode.cloudcode') - -user = context.local('user') +__author__ = "asaka " + +logger = logging.getLogger("leancloud.cloudcode.cloudcode") + +user = context.local("user") +current = context.local("current") + +# http.HTTPStatus is not available in Python 2 +http_status_codes = { + 100, + 101, + 102, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 226, + 300, + 301, + 302, + 303, + 304, + 305, + 307, + 308, + 400, + 401, + 402, + 403, + 404, + 405, + 406, + 407, + 408, + 409, + 410, + 411, + 412, + 413, + 414, + 415, + 416, + 417, + 421, + 422, + 423, + 424, + 426, + 428, + 429, + 431, + 451, + 500, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 510, + 511, +} class LeanEngineError(Exception): - def __init__(self, code=1, message='error'): - self.code = code - self.message = message + def __init__( + self, + code=None, # for backward compatibility, should be 400 + message="error", # for backward compatibility, should be required + status=None, # for backward compatibility, should be 400 + ): + if status is None: + if isinstance(code, six.string_types): + self.message = code + self.code = 400 # for backward compatibility, should be 1 + self.status = 400 + else: + self.message = message + # for backward compatibility, should be 1 + self.code = 400 if code is None else code + self.status = self.code if self.code in http_status_codes else 400 + else: + if isinstance(code, six.string_types): + self.message = code + self.code = 400 + self.status = status + else: + self.message = message + self.code = 1 if code is None else code + self.status = status class LeanEngineApplication(object): - def __init__(self): - self.url_map = Map([ - Rule('/__engine/1/functions/', endpoint='cloud_function'), - Rule('/__engine/1.1/functions/', endpoint='cloud_function'), - Rule('/__engine/1/call/', endpoint='rpc_function'), - Rule('/__engine/1.1/call/', endpoint='rpc_function'), - Rule('/__engine/1/functions/BigQuery/', endpoint='on_bigquery'), - Rule('/__engine/1.1/functions/BigQuery/', endpoint='on_bigquery'), - Rule('/__engine/1.1/functions/_User/onLogin', endpoint='on_login'), - Rule('/__engine/1/functions/_User/onLogin', endpoint='on_login'), - Rule('/__engine/1/functions//', endpoint='cloud_hook'), - Rule('/__engine/1.1/functions//', endpoint='cloud_hook'), - Rule('/__engine/1/functions/onVerified/', endpoint='on_verified'), - Rule('/__engine/1.1/functions/onVerified/', endpoint='on_verified'), - Rule('/__engine/1/functions/_ops/metadatas', endpoint='ops_meta_data'), - Rule('/__engine/1.1/functions/_ops/metadatas', endpoint='ops_meta_data'), - - Rule('/1/functions/', endpoint='cloud_function'), - Rule('/1.1/functions/', endpoint='cloud_function'), - Rule('/1/call/', endpoint='rpc_function'), - Rule('/1.1/call/', endpoint='rpc_function'), - Rule('/1/functions/BigQuery/', endpoint='on_bigquery'), - Rule('/1.1/functions/BigQuery/', endpoint='on_bigquery'), - Rule('/1.1/functions/_User/onLogin', endpoint='on_login'), - Rule('/1/functions/_User/onLogin', endpoint='on_login'), - Rule('/1/functions//', endpoint='cloud_hook'), - Rule('/1.1/functions//', endpoint='cloud_hook'), - Rule('/1/functions/onVerified/', endpoint='on_verified'), - Rule('/1.1/functions/onVerified/', endpoint='on_verified'), - Rule('/1/functions/_ops/metadatas', endpoint='ops_meta_data'), - Rule('/1.1/functions/_ops/metadatas', endpoint='ops_meta_data'), - ]) + def __init__(self, fetch_user): + self.fetch_user = fetch_user + self.url_map = Map( + [ + Rule("/__engine/1/functions/", endpoint="cloud_function"), + Rule("/__engine/1.1/functions/", endpoint="cloud_function"), + Rule("/__engine/1/call/", endpoint="rpc_function"), + Rule("/__engine/1.1/call/", endpoint="rpc_function"), + Rule("/__engine/1/functions/BigQuery/", endpoint="on_bigquery"), + Rule( + "/__engine/1.1/functions/BigQuery/", endpoint="on_bigquery" + ), + Rule("/__engine/1.1/functions/_User/onLogin", endpoint="on_login"), + Rule("/__engine/1/functions/_User/onLogin", endpoint="on_login"), + Rule("/__engine/1.1/functions/_User/onAuthData", endpoint="on_auth_data"), + Rule("/__engine/1/functions/_User/onAuthData", endpoint="on_auth_data"), + Rule( + "/__engine/1/functions//", + endpoint="cloud_hook", + ), + Rule( + "/__engine/1.1/functions//", + endpoint="cloud_hook", + ), + Rule( + "/__engine/1/functions/onVerified/", + endpoint="on_verified", + ), + Rule( + "/__engine/1.1/functions/onVerified/", + endpoint="on_verified", + ), + Rule("/__engine/1/functions/_ops/metadatas", endpoint="ops_meta_data"), + Rule( + "/__engine/1.1/functions/_ops/metadatas", endpoint="ops_meta_data" + ), + Rule("/1/functions/", endpoint="cloud_function"), + Rule("/1.1/functions/", endpoint="cloud_function"), + Rule("/1/call/", endpoint="rpc_function"), + Rule("/1.1/call/", endpoint="rpc_function"), + Rule("/1/functions/BigQuery/", endpoint="on_bigquery"), + Rule("/1.1/functions/BigQuery/", endpoint="on_bigquery"), + Rule("/1.1/functions/_User/onLogin", endpoint="on_login"), + Rule("/1/functions/_User/onLogin", endpoint="on_login"), + Rule("/1.1/functions/_User/onAuthData", endpoint="on_auth_data"), + Rule("/1/functions/_User/onAuthData", endpoint="on_auth_data"), + Rule("/1/functions//", endpoint="cloud_hook"), + Rule("/1.1/functions//", endpoint="cloud_hook"), + Rule("/1/functions/onVerified/", endpoint="on_verified"), + Rule("/1.1/functions/onVerified/", endpoint="on_verified"), + Rule("/1/functions/_ops/metadatas", endpoint="ops_meta_data"), + Rule("/1.1/functions/_ops/metadatas", endpoint="ops_meta_data"), + ] + ) + self.cloud_codes = {} def __call__(self, environ, start_response): self.process_session(environ) - request = environ['leanengine.request'] - - response = self.dispatch_request(request) - + response = self.dispatch_request(environ) return response(environ, start_response) - @classmethod - def process_session(cls, environ): - if environ['_app_params']['session_token'] not in (None, ''): - session_token = environ['_app_params']['session_token'] - user = leancloud.User.become(session_token) - context.local.user = user + def process_session(self, environ): + request = environ["leanengine.request"] + context.local.current = context.Current() + context.local.current.meta = { + "remote_address": get_remote_address(request), + } + + if environ["_app_params"]["session_token"] not in (None, ""): + session_token = environ["_app_params"]["session_token"] + if self.fetch_user: + user = leancloud.User.become(session_token) + context.local.current.user = user + context.local.user = user + context.local.current.session_token = session_token return - request = environ['leanengine.request'] try: - # the JSON object must be str, not 'bytes' for 3.x. - data = json.loads(to_native(request.get_data())) + data = json.loads(request.get_data(as_text=True)) except ValueError: context.local.user = None return - if 'user' in data and data['user']: + if "user" in data and data["user"]: user = leancloud.User() - user._update_data(data['user']) + user._update_data(data["user"]) + context.local.current.user = user + context.local.current.session_token = user.get_session_token() context.local.user = user return context.local.user = None - def dispatch_request(self, request): + def dispatch_request(self, environ): + request = environ["leanengine.request"] + app_params = environ["_app_params"] adapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = adapter.match() except HTTPException as e: return e - # the JSON object must be str, not 'bytes' for 3.x. - params = to_native(request.get_data()) - values['params'] = json.loads(params) if params != '' else {} + params = request.get_data(as_text=True) + values["params"] = json.loads(params) if params != "" else {} try: - if endpoint == 'cloud_function': - result = {'result': dispatch_cloud_func(decode_object=False, **values)} - elif endpoint == 'rpc_function': - result = {'result': dispatch_cloud_func(decode_object=True, **values)} - elif endpoint == 'cloud_hook': - result = dispatch_cloud_hook(**values) - elif endpoint == 'on_verified': - result = {'result': dispatch_on_verified(**values)} - elif endpoint == 'on_login': - result = {'result': dispatch_on_login(**values)} - elif endpoint == 'ops_meta_data': - result = {'result': dispatch_ops_meta_data()} - elif endpoint == 'on_bigquery': - result = {'result': dispatch_on_bigquery(**values)} + if endpoint == "cloud_function": + result = { + "result": dispatch_cloud_func( + self.cloud_codes, app_params, decode_object=False, **values + ) + } + elif endpoint == "rpc_function": + result = { + "result": dispatch_cloud_func( + self.cloud_codes, app_params, decode_object=True, **values + ) + } + elif endpoint == "cloud_hook": + result = dispatch_cloud_hook(self.cloud_codes, app_params, **values) + elif endpoint == "on_verified": + result = { + "result": dispatch_on_verified( + self.cloud_codes, app_params, **values + ) + } + elif endpoint == "on_login": + result = { + "result": dispatch_on_login(self.cloud_codes, app_params, **values) + } + elif endpoint == "on_auth_data": + result = { + "result": dispatch_on_auth_data(self.cloud_codes, app_params, **values) + } + elif endpoint == "ops_meta_data": + from .authorization import MASTER_KEY + + if ( + request.environ.get("_app_params", {}).get("master_key") + != MASTER_KEY + ): + raise LeanEngineError(code=401, message="Unauthorized.") + result = {"result": dispatch_ops_meta_data(self.cloud_codes)} + elif endpoint == "on_bigquery": + result = { + "result": dispatch_on_bigquery( + self.cloud_codes, app_params, **values + ) + } else: - raise ValueError # impossible - return Response(json.dumps(result), mimetype='application/json') + raise ValueError # impossible + return Response(json.dumps(result), mimetype="application/json") except LeanEngineError as e: return Response( - json.dumps({'code': e.code, 'error': e.message}), - status=400, - mimetype='application/json' + json.dumps({"code": e.code, "error": e.message}), # noqa: B306 + status=e.status, + mimetype="application/json", ) except Exception: - print(traceback.format_exc()) + print(traceback.format_exc(), file=sys.stderr) return Response( - json.dumps({'code': 141, 'error': 'Cloud Code script had an error.'}), + json.dumps({"code": 141, "error": "Cloud Code script had an error."}), status=500, - mimetype='application/json' + mimetype="application/json", + ) + + def update_cloud_codes(self, engine_cloud_codes): + already_register_func_name = set(self.cloud_codes.keys()).intersection( + set(engine_cloud_codes.keys()) + ) + if already_register_func_name: + is_are = "is" if len(already_register_func_name) == 1 else "are" + raise RuntimeError( + "cloud function: {0} {1} already registerd.".format( + ",".join(already_register_func_name), is_are + ) ) + self.cloud_codes.update(engine_cloud_codes) hook_name_mapping = { - 'beforeSave': '__before_save_for_', - 'afterSave': '__after_save_for_', - 'beforeUpdate': '__before_update_for_', - 'afterUpdate': '__after_update_for_', - 'beforeDelete': '__before_delete_for_', - 'afterDelete': '__after_delete_for_', + "beforeSave": "__before_save_for_", + "afterSave": "__after_save_for_", + "beforeUpdate": "__before_update_for_", + "afterUpdate": "__after_update_for_", + "beforeDelete": "__before_delete_for_", + "afterDelete": "__after_delete_for_", } -_cloud_codes = {} +root_engine = None + + +def register_cloud_func(_cloud_codes, func_or_func_name): + if isinstance(func_or_func_name, six.string_types): + func_name = func_or_func_name + + def inner_func(func): + if func_name in _cloud_codes: + raise RuntimeError( + "cloud function: {0} is already registered".format(func_name) + ) + _cloud_codes[func_name] = func + return func + return inner_func -def register_cloud_func(func): + func = func_or_func_name func_name = func.__name__ if func_name in _cloud_codes: - raise RuntimeError('cloud function: {0} is already registered'.format(func_name)) + raise RuntimeError( + "cloud function: {0} is already registered".format(func_name) + ) _cloud_codes[func_name] = func return func -def dispatch_cloud_func(func_name, decode_object, params): +def dispatch_cloud_func(_cloud_codes, app_params, func_name, decode_object, params): + # let's check realtime hook sign first + realtime_hook_funcs = [ + "_messageReceived", + "_receiversOffline", + "_messageSent", + "_messageUpdate", + "_conversationStart", + "_conversationStarted", + "_conversationAdd", + "_conversationAdded", + "_conversationRemove", + "_conversationRemoved", + "_conversationUpdate", + "_clientOnline", + "_clientOffline", + "_rtmClientSign", + ] + from .authorization import HOOK_KEY + + if func_name in realtime_hook_funcs: + current_hook_key = app_params.get("hook_key") + if not current_hook_key or current_hook_key != HOOK_KEY: + raise LeanEngineError(code=401, message="Unauthorized.") + # delete all keys in params which starts with low dash. # JS SDK may send it's app info with them. - keys = params.keys() - for key in keys: - if key.startswith('_') and key != '__type': - params.pop(key) + params = { + k: v for k, v in params.items() if (not k.startswith("_")) or k == "__type" + } if decode_object: - params = leancloud.utils.decode('', params) + params = leancloud.utils.decode("", params) func = _cloud_codes.get(func_name) if not func: - raise LeanEngineError(code=404, message="cloud func named '{0}' not found.".format(func_name)) + raise LeanEngineError( + code=404, message="cloud func named '{0}' not found.".format(func_name) + ) logger.info("{0} is called!".format(func_name)) result = func(**params) if decode_object: - if isinstance(result, leancloud.Object): - result = leancloud.utils.encode(result._dump()) - else: - result = leancloud.utils.encode(result) + result = leancloud.utils.encode(result, dump_objects=True) return result -def register_cloud_hook(class_name, hook_name): +def register_cloud_hook(_cloud_codes, class_name, hook_name): # hack the hook name hook_name = hook_name_mapping[hook_name] + class_name if hook_name in _cloud_codes: - raise RuntimeError('cloud hook {0} on class {1} is already registered'.format(hook_name, class_name)) + raise RuntimeError( + "cloud hook {0} on class {1} is already registered".format( + hook_name, class_name + ) + ) def new_func(func): _cloud_codes[hook_name] = func @@ -200,38 +404,43 @@ def new_func(func): return new_func -before_save = functools.partial(register_cloud_hook, hook_name='beforeSave') +before_save = functools.partial(register_cloud_hook, hook_name="beforeSave") + +after_save = functools.partial(register_cloud_hook, hook_name="afterSave") -after_save = functools.partial(register_cloud_hook, hook_name='afterSave') +before_update = functools.partial(register_cloud_hook, hook_name="beforeUpdate") -before_update = functools.partial(register_cloud_hook, hook_name='beforeUpdate') +after_update = functools.partial(register_cloud_hook, hook_name="afterUpdate") -after_update = functools.partial(register_cloud_hook, hook_name='afterUpdate') +before_delete = functools.partial(register_cloud_hook, hook_name="beforeDelete") -before_delete = functools.partial(register_cloud_hook, hook_name='beforeDelete') +after_delete = functools.partial(register_cloud_hook, hook_name="afterDelete") -after_delete = functools.partial(register_cloud_hook, hook_name='afterDelete') +def dispatch_cloud_hook(_cloud_codes, app_params, class_name, hook_name, params): + from .authorization import HOOK_KEY -def dispatch_cloud_hook(class_name, hook_name, params): + current_hook_key = app_params.get("hook_key") + if not current_hook_key or current_hook_key != HOOK_KEY: + raise LeanEngineError(code=401, message="Unauthorized.") hook_name = hook_name_mapping[hook_name] + class_name if hook_name not in _cloud_codes: raise NotAcceptable obj = leancloud.Object.create(class_name) - obj._update_data(params['object']) + obj._update_data(params["object"]) - if '__updateKeys' in params['object']: - obj.updated_keys = params['object']['__updateKeys'] + if "_updatedKeys" in params["object"]: + obj.updated_keys = params["object"]["_updatedKeys"] - if hook_name.startswith('__before'): - if obj.has('__before'): - obj.set('__before', obj.get('__before')) + if hook_name.startswith("__before"): + if obj.has("__before"): + obj.set("__before", obj.get("__before")) else: obj.disable_before_hook() - elif hook_name.startswith('__after'): - if obj.has('__after'): - obj.set('__after', obj.get('__after')) + elif hook_name.startswith("__after"): + if obj.has("__after"): + obj.set("__after", obj.get("__after")) else: obj.disable_after_hook() @@ -239,83 +448,136 @@ def dispatch_cloud_hook(class_name, hook_name, params): func = _cloud_codes[hook_name] if not func: - raise leancloud.LeanEngineError(code=404, message="cloud hook named '{0}' not found.".format(hook_name)) + raise leancloud.LeanEngineError( + code=404, message="cloud hook named '{0}' not found.".format(hook_name) + ) func(obj) - if hook_name.startswith('__after'): - return {'result': 'ok'} - elif hook_name.startswith('__before_delete_for'): + if hook_name.startswith("__after"): + return {"result": "ok"} + elif hook_name.startswith("__before_delete_for"): return {} else: return obj.dump() -def register_on_verified(verify_type): - if verify_type not in set(['sms', 'email']): - raise RuntimeError('verify_type must be sms or email') +def register_on_verified(_cloud_codes, verify_type): + if verify_type not in set(["sms", "email"]): + raise RuntimeError("verify_type must be sms or email") - func_name = '__on_verified_{0}'.format(verify_type) + func_name = "__on_verified_{0}".format(verify_type) def new_func(func): if func_name in _cloud_codes: - raise RuntimeError('on verified is already registered') + raise RuntimeError("on verified is already registered") _cloud_codes[func_name] = func + return new_func -def dispatch_on_verified(verify_type, user): - func = _cloud_codes.get(verify_type) +def dispatch_on_verified(_cloud_codes, app_params, verify_type, params): + func_name = "__on_verified_" + verify_type + from .authorization import HOOK_KEY + + hook_key = app_params.get("hook_key") + if not hook_key or hook_key != HOOK_KEY: + raise LeanEngineError(code=401, message="Unauthorized.") + + user = leancloud.User() + user._update_data(params["object"]) + + func = _cloud_codes.get(func_name) if not func: return - return func(user) -def register_on_login(func): - func_name = '__on_login__User' +def register_on_login(_cloud_codes, func): + func_name = "__on_login__User" if func_name in _cloud_codes: - raise RuntimeError('on login is already registered') + raise RuntimeError("on login is already registered") _cloud_codes[func_name] = func -def dispatch_on_login(params): - func = _cloud_codes.get('__on_login__User') +def dispatch_on_login(_cloud_codes, app_params, params): + from .authorization import HOOK_KEY + + current_hook_key = app_params.get("hook_key") + if not current_hook_key or current_hook_key != HOOK_KEY: + raise LeanEngineError(code=401, message="Unauthorized.") + + func = _cloud_codes.get("__on_login__User") if not func: return user = leancloud.User() - user._update_data(params['object']) + user._update_data(params["object"]) return func(user) +def register_on_auth_data(_cloud_codes, func): + func_name = "__on_authdata__User" -def dispatch_ops_meta_data(): + if func_name in _cloud_codes: + raise RuntimeError("on authdata is already registered") + _cloud_codes[func_name] = func + +def dispatch_on_auth_data(_cloud_codes, app_params, params): + from .authorization import HOOK_KEY + current_hook_key = app_params.get("hook_key") + if not current_hook_key or current_hook_key != HOOK_KEY: + raise LeanEngineError(code=401, message="Unauthorized.") + + func = _cloud_codes.get("__on_authdata__User") + if not func: + return + + auth_data = params["authData"] + return func(auth_data) + + +def dispatch_ops_meta_data(_cloud_codes): return list(_cloud_codes.keys()) -def register_on_bigquery(event): - if event == 'end': - func_name = '__on_complete_bigquery_job' +def register_on_bigquery(_cloud_codes, event): + if event == "end": + func_name = "__on_complete_bigquery_job" else: - raise RuntimeError('event not support') + raise RuntimeError("event not support") def inner_func(func): if func_name in _cloud_codes: - raise RuntimeError('on bigquery is already registered') + raise RuntimeError("on bigquery is already registered") _cloud_codes[func_name] = func + return inner_func -def dispatch_on_bigquery(event, params): - if event == 'onComplete': - func_name = '__on_complete_bigquery_job' +def dispatch_on_bigquery(_cloud_codes, app_params, event, params): + if event == "onComplete": + func_name = "__on_complete_bigquery_job" else: return + from .authorization import HOOK_KEY + + current_hook_key = app_params.get("hook_key") + if not current_hook_key or current_hook_key != HOOK_KEY: + raise LeanEngineError(code=401, message="Unauthorized.") + func = _cloud_codes.get(func_name) if not func: return - ok = True if params['status'] == 'OK' else False + ok = True if params["status"] == "OK" else False return func(ok, params) + + +def get_remote_address(request): + return ( + request.headers.get("x-real-ip") + or request.headers.get("x-forwarded-for") + or request.remote_addr + ) diff --git a/leancloud/engine/utils.py b/leancloud/engine/utils.py index 7c217146..37dcf39d 100644 --- a/leancloud/engine/utils.py +++ b/leancloud/engine/utils.py @@ -1,16 +1,34 @@ # coding: utf-8 +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + import hashlib -from leancloud._compat import to_bytes +import six + -__author__ = 'asaka ' +__author__ = "asaka " def sign_by_key(timestamp, key): - return hashlib.md5(to_bytes('{0}{1}'.format(timestamp, key))).hexdigest() + s = "{0}{1}".format(timestamp, key) + return hashlib.md5(s.encode("utf-8")).hexdigest() + + +if six.PY2: + + def to_native(s): + if isinstance(s, unicode): # noqa: F821 + return s.encode("utf-8") + return s + +else: -def sign_disable_hook(hook_name, master_key, timestamp): - sign = hashlib.sha1(to_bytes('{0}{1}:{2}'.format(master_key, hook_name, timestamp))).hexdigist() - return '{0},{1}'.format(timestamp, sign) + def to_native(s): + if isinstance(s, bytes): + return s.decode("utf-8") + return s diff --git a/leancloud/errors.py b/leancloud/errors.py index d142ac61..7a5366b5 100644 --- a/leancloud/errors.py +++ b/leancloud/errors.py @@ -3,18 +3,26 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals -__author__ = 'asaka ' +import six +__author__ = "asaka " + +@six.python_2_unicode_compatible class LeanCloudError(Exception): def __init__(self, code, error): self.code = code self.error = error def __str__(self): - error = self.error if isinstance(self.error, str) else self.error.encode('utf-8', 'ignore') - return 'LeanCloudError: [{0}] {1}'.format(self.code, error) + error = ( + self.error + if isinstance(self.error, six.text_type) + else self.error.encode("utf-8", "ignore") + ) + return "LeanCloudError: [{0}] {1}".format(self.code, error) class LeanCloudWarning(UserWarning): diff --git a/leancloud/fields.py b/leancloud/fields.py deleted file mode 100644 index 1a58d8c4..00000000 --- a/leancloud/fields.py +++ /dev/null @@ -1,19 +0,0 @@ -# coding: utf-8 - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -__author__ = 'asaka ' - - -class AnyField(object): - pass - - -class StringField(AnyField): - pass - - -class NumberField(AnyField): - pass diff --git a/leancloud/file_.py b/leancloud/file_.py index f3bf5165..b97322fe 100644 --- a/leancloud/file_.py +++ b/leancloud/file_.py @@ -3,99 +3,126 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import os import re -import base64 -import codecs -import random +import io import hashlib +import logging +import threading +import six import requests import leancloud from leancloud import client -from leancloud._compat import BytesIO -from leancloud._compat import PY2 -from leancloud._compat import range_type -from leancloud._compat import file_type -from leancloud._compat import buffer_type -from leancloud.mime_type import mime_types +from leancloud import utils from leancloud.errors import LeanCloudError -__author__ = 'asaka ' +__author__ = "asaka " + +logger = logging.getLogger(__name__) + + +DEFAULT_TIMEOUT = 30 class File(object): - def __init__(self, name, data=None, type_=None): + _class_name = "_File" # walks like a leancloud.Object + + def __init__(self, name="", data=None, mime_type=None): self._name = name + self.key = None self.id = None + self.created_at = None + self.updated_at = None self._url = None + self._successful_url = None self._acl = None - self.current_user = None # TODO - self._metadata = { - 'owner': 'unknown' - } - if self.current_user and self.current_user is not None: - self._metadata['owner'] = self.current_user.id - - pattern = re.compile('\.([^.]*)$') + self.current_user = leancloud.User.get_current() + self.timeout = 30 + self._metadata = {"owner": "unknown"} + if ( + self.current_user and self.current_user != None + ): # NOQA: self.current_user may be a thread_local object + self._metadata["owner"] = self.current_user.id + + pattern = re.compile(r"\.([^.]*)$") extension = pattern.findall(name) if extension: self.extension = extension[0].lower() else: - self.extension = '' + self.extension = "" - if type_: - self._type = type_ - else: - self._type = mime_types.get(self.extension, 'text/plain') + self._mime_type = mime_type if data is None: self._source = None - elif isinstance(data, BytesIO): - self._source = data - elif isinstance(data, file_type): + return + + try: + data.read + data.tell + data.seek(0, os.SEEK_END) data.seek(0, os.SEEK_SET) - self._source = BytesIO(data.read()) - elif isinstance(data, buffer_type): - self._source = BytesIO(data) - elif PY2: - import cStringIO - if isinstance(data, cStringIO.OutputType): - data.seek(0, os.SEEK_SET) - self._source = BytesIO(data.getvalue()) + except Exception: + if (six.PY3 and isinstance(data, (memoryview, bytes))) or ( + six.PY2 and isinstance(data, (buffer, memoryview, str)) # noqa: F821 + ): + data = io.BytesIO(data) + elif data.read: + data = io.BytesIO(data.read()) else: - raise TypeError('data must be a StringIO / buffer / file instance') + raise TypeError( + "Do not know how to handle data, accepts file like object or bytes" + ) - else: - raise TypeError('data must be a StringIO / buffer / file instance') + data.seek(0, os.SEEK_SET) + checksum = hashlib.md5() + while True: + chunk = data.read(4096) + if not chunk: + break + + try: + checksum.update(chunk) + except TypeError: + checksum.update(chunk.encode("utf-8")) + + self._metadata["_checksum"] = checksum.hexdigest() + self._metadata["size"] = data.tell() + + # 3.5MB, 1Mbps * 30s + # increase timeout + if self._metadata["size"] > 3750000: + self.timeout = self.timeout * int(self._metadata["size"] / 3750000) + + data.seek(0, os.SEEK_SET) + + self._source = data - if self._source: - self._source.seek(0, os.SEEK_END) - self._metadata['size'] = self._source.tell() - self._source.seek(0, os.SEEK_SET) - checksum = hashlib.md5() - checksum.update(self._source.getvalue()) - self._metadata['_checksum'] = checksum.hexdigest() + @utils.classproperty + def query(self): + return leancloud.Query(self) @classmethod - def create_with_url(cls, name, url, meta_data=None, type_=None): - f = File(name, None, type_) + def create_with_url(cls, name, url, meta_data=None, mime_type=None): + f = File(name, None, mime_type) if meta_data: f._metadata.update(meta_data) - if isinstance(url, str): + if isinstance(url, six.string_types): f._url = url else: - raise ValueError('url must be a string') + raise ValueError("url must be a str / unicode") - f._metadata['__source'] = 'external' + f._metadata["__source"] = "external" return f @classmethod def create_without_data(cls, object_id): - f = File('') + f = File("") f.id = object_id return f @@ -104,7 +131,7 @@ def get_acl(self): def set_acl(self, acl): if not isinstance(acl, leancloud.ACL): - raise TypeError('acl must be a leancloud.ACL instance') + raise TypeError("acl must be a leancloud.ACL instance") self._acl = acl @property @@ -113,131 +140,197 @@ def name(self): @property def url(self): - return self._url + return self._successful_url + + @property + def mime_type(self): + return self._mime_type + + @mime_type.setter + def set_mime_type(self, mime_type): + self._mime_type = mime_type @property def size(self): - return self._metadata['size'] + return self._metadata["size"] @property def owner_id(self): - return self._metadata['owner'] + return self._metadata["owner"] @property def metadata(self): return self._metadata - def get_thumbnail_url(self, width, height, quality=100, scale_to_fit=True, fmt='png'): - if not self._url: - raise ValueError('invalid url') + def get_thumbnail_url( + self, width, height, quality=100, scale_to_fit=True, fmt="png" + ): + if not self.url: + raise ValueError("invalid url") if width < 0 or height < 0: - raise ValueError('invalid height or width params') + raise ValueError("invalid height or width params") if quality > 100 or quality <= 0: - raise ValueError('quality must between 0 and 100') + raise ValueError("quality must between 0 and 100") mode = 2 if scale_to_fit else 1 - return self.url + '?imageView/{0}/w/{1}/h/{2}/q/{3}/format/{4}'.format(mode, width, height, quality, fmt) + return self.url + "?imageView/{0}/w/{1}/h/{2}/q/{3}/format/{4}".format( + mode, width, height, quality, fmt + ) def destroy(self): if not self.id: return False - response = client.delete('/files/{0}'.format(self.id)) + response = client.delete("/files/{0}".format(self.id)) if response.status_code != 200: raise LeanCloudError(1, "the file is not sucessfully destroyed") - def _save_to_qiniu(self, uptoken, key): - import qiniu + + def _save_to_qiniu(self, token, key): self._source.seek(0) - ret, info = qiniu.put_data(uptoken, key, self._source) + + import qiniu + + qiniu.set_default(connection_timeout=self.timeout) + ret, info = qiniu.put_data(token, key, self._source) self._source.seek(0) if info.status_code != 200: - raise LeanCloudError(1, 'the file is not saved, qiniu status code: {0}'.format(info.status_code)) - - def _save_to_s3(self, upload_url): + self._save_callback(token, False) + raise LeanCloudError( + 1, + "the file is not saved, qiniu status code: {0}".format( + info.status_code + ), + ) + self._save_callback(token, True) + + def _save_to_s3(self, token, upload_url): self._source.seek(0) - responce = requests.put(upload_url, data=self._source.getvalue(), headers={'Content-Type':self._type}) - if responce.status_code != 200: - raise LeanCloudError(1, 'The file is not successfully saved to Qcloud') + response = requests.put( + upload_url, data=self._source, headers={"Content-Type": self.mime_type} + ) + if response.status_code != 200: + self._save_callback(token, False) + raise LeanCloudError(1, "The file is not successfully saved to S3") self._source.seek(0) + self._save_callback(token, True) def _save_external(self): data = { - 'name': self._name, - 'ACL': self._acl, - 'metaData': self._metadata, - 'mime_type': self._type, - 'url': self._url, + "name": self._name, + "ACL": self._acl, + "metaData": self._metadata, + "mime_type": self.mime_type, + "url": self._url, } - response = client.post('/files/{0}'.format(self._name), data) + response = client.post("/files".format(self._name), data) content = response.json() - self._name = content['name'] - self._url = content['url'] - self.id = content['objectId'] - if 'size' in content: - self._metadata['size'] = content['size'] - else: - raise ValueError + self.id = content["objectId"] - def _save_to_qcloud(self, uptoken, upload_url): + self._successful_url = self._url + + _created_at = utils.decode_date_string(content.get("createdAt")) + _updated_at = utils.decode_updated_at(content.get("updatedAt"), _created_at) + if _created_at is not None: + self.created_at = _created_at + if _updated_at is not None: + self.updated_at = _updated_at + + def _save_to_qcloud(self, token, upload_url): headers = { - 'Authorization': uptoken, + "Authorization": token, } self._source.seek(0) data = { - 'op': 'upload', - 'filecontent': self._source.read(), + "op": "upload", + "filecontent": self._source.read(), } response = requests.post(upload_url, headers=headers, files=data) self._source.seek(0) info = response.json() - if info['code'] != 0: - raise LeanCloudError(1, 'this file is not saved, qcloud cos status code: {}'.format(info['code'])) + if info["code"] != 0: + self._save_callback(token, False) + raise LeanCloudError( + 1, + "this file is not saved, qcloud cos status code: {}".format( + info["code"] + ), + ) + self._save_callback(token, True) + + def _save_callback(self, token, successed): + if not token: + return + + def f(): + try: + client.post("/fileCallback", {"token": token, "result": successed}) + except LeanCloudError as e: + logger.warning("call file callback failed, error: %s", e) + + threading.Thread(target=f).start() def save(self): - if self._url and self.metadata.get('__source') == 'external': + if self._url and self.metadata.get("__source") == "external": self._save_external() elif not self._source: pass else: content = self._get_file_token() - if content['provider'] == 'qiniu': - self._save_to_qiniu(content['token'], content['key']) - elif content['provider']== 'qcloud': - self._save_to_qcloud(content['token'], content['upload_url']) - elif content['provider'] == 's3': - self._save_to_s3(content['upload_url']) + self._mime_type = content["mime_type"] + if content["provider"] == "qiniu": + self._save_to_qiniu(content["token"], content["key"]) + elif content["provider"] == "qcloud": + self._save_to_qcloud(content["token"], content["upload_url"]) + elif content["provider"] == "s3": + self._save_to_s3(content.get("token"), content["upload_url"]) else: - raise RuntimeError('The provider field in the fetched content is empty') + raise RuntimeError("The provider field in the fetched content is empty") + self._update_data(content) + + def _update_data(self, server_data): + if "objectId" in server_data: + self.id = server_data.get("objectId") + if "name" in server_data: + self._name = server_data.get("name") + if "url" in server_data: + self._url = server_data.get("url") + self._successful_url = self._url + if "key" in server_data: + self.key = server_data.get("key") + if "mime_type" in server_data: + self._mime_type = server_data["mime_type"] + if "metaData" in server_data: + self._metadata = server_data.get("metaData") + + _created_at = utils.decode_date_string(server_data.get("createdAt")) + _updated_at = utils.decode_updated_at(server_data.get("updatedAt"), _created_at) + if _created_at is not None: + self.created_at = _created_at + if _updated_at is not None: + self.updated_at = _updated_at def _get_file_token(self): - hex_octet = lambda: hex(int(0x10000 * (1 + random.random())))[-4:] - key = ''.join(hex_octet() for _ in range(4)) - key = '{0}.{1}'.format(key, self.extension) data = { - 'name': self._name, - 'key': key, - 'ACL': self._acl, - 'mime_type': self._type, - 'metaData': self._metadata, + "name": self._name, + "ACL": self._acl, + "mime_type": self.mime_type, + "metaData": self._metadata, } - response = client.post('/fileTokens', data) + if self.key is not None: + data["key"] = self.key + response = client.post("/fileTokens", data) content = response.json() - self.id = content['objectId'] - self._url = content['url'] - content['key'] = key + self.id = content["objectId"] + self._url = content["url"] + self.key = content["key"] return content - def fetch(self): - response = client.get('/files/{0}'.format(self.id)) + response = client.get("/files/{0}".format(self.id)) content = response.json() - self._name = content.get('name') - self.id = content.get('objectId') - self._url = content.get('url') - self._type = content.get('mime_type') - self._metadata = content.get('metaData') + self._update_data(content) diff --git a/leancloud/geo_point.py b/leancloud/geo_point.py index 87418c8a..786d2461 100644 --- a/leancloud/geo_point.py +++ b/leancloud/geo_point.py @@ -3,11 +3,12 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import math -__author__ = 'asaka ' +__author__ = "asaka " class GeoPoint(object): @@ -27,16 +28,16 @@ def __init__(self, latitude=0, longitude=0): @classmethod def _validate(cls, latitude, longitude): if latitude < -90.0: - raise ValueError('GeoPoint latitude {0} < -90.0'.format(latitude)) + raise ValueError("GeoPoint latitude {0} < -90.0".format(latitude)) if latitude > 90.0: - raise ValueError('GeoPoint latitude {0} > 90.0'.format(latitude)) + raise ValueError("GeoPoint latitude {0} > 90.0".format(latitude)) if longitude < -180.0: - raise ValueError('GeoPoint longitude {0} < -180.0'.format(longitude)) + raise ValueError("GeoPoint longitude {0} < -180.0".format(longitude)) if longitude > 180.0: - raise ValueError('GeoPoint longitude {0} > 180.0'.format(longitude)) + raise ValueError("GeoPoint longitude {0} > 180.0".format(longitude)) @property def latitude(self): @@ -65,9 +66,9 @@ def longitude(self, longitude): def dump(self): self._validate(self.latitude, self.longitude) return { - '__type': 'GeoPoint', - 'latitude': self.latitude, - 'longitude': self.longitude, + "__type": "GeoPoint", + "latitude": self.latitude, + "longitude": self.longitude, } def radians_to(self, other): @@ -91,9 +92,12 @@ def radians_to(self, other): sin_delta_lat_div2 = math.sin(delta_lat / 2.0) sin_delta_long_div2 = math.sin(delta_long / 2.0) - a = ((sin_delta_lat_div2 * sin_delta_lat_div2) + - (math.cos(lat1rad) * math.cos(lat2rad) * - sin_delta_long_div2 * sin_delta_long_div2)) + a = (sin_delta_lat_div2 * sin_delta_lat_div2) + ( + math.cos(lat1rad) + * math.cos(lat2rad) + * sin_delta_long_div2 + * sin_delta_long_div2 + ) a = min(1.0, a) return 2 * math.asin(math.sqrt(a)) @@ -118,7 +122,8 @@ def miles_to(self, other): return self.radians_to(other) * 3958.8 def __eq__(self, other): - return \ - isinstance(other, GeoPoint) and \ - self.latitude == other.latitude and \ - self.longitude == other.longitude + return ( + isinstance(other, GeoPoint) + and self.latitude == other.latitude + and self.longitude == other.longitude + ) diff --git a/leancloud/message.py b/leancloud/message.py new file mode 100644 index 00000000..b4d21a08 --- /dev/null +++ b/leancloud/message.py @@ -0,0 +1,144 @@ +# coding: utf-8 + +""" +实时通讯消息相关操作。 +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from datetime import datetime +from typing import Any +from typing import Dict +from typing import Generator +from typing import List +from typing import Optional +from typing import Union + +import six + +from leancloud import client + + +class Message(object): + def __init__(self): + self.bin = None # type: bool + self.conversation_id = None # type: str + self.data = None # type: str + self.from_client = None # type: str + self.from_ip = None # type: str + self.is_conversation = None # type: bool + self.is_room = None # type: bool + self.message_id = None # type: str + self.timestamp = None # type: float + self.to = None # type: str + + @classmethod + def _find(cls, query_params): # type: (dict) -> Generator[Message, None, None] + content = client.get("/rtm/messages/history", params=query_params).json() + for data in content: + msg = cls() + msg._update_data(data) + yield msg + + @classmethod + def find_by_conversation( + cls, + conversation_id, + limit=None, + reversed=None, + before_time=None, + before_message_id=None, + ): + # type: (str, Optional[int], Optional[bool], Optional[Union[datetime, float]], Optional[str]) -> List[Message] # noqa: E501 + """获取某个对话中的聊天记录 + + :param conversation_id: 对话 id + :param limit: 返回条数限制,可选,服务端默认 100 条,最大 1000 条 + :param reversed: 以默认排序(查找更老的历史消息)相反的方向返回结果(也即从某条消息记录开始查找更新的消息),服务端默认为 False + 如果 reversed = True, + 则 before_time/before_message_id 转变成最老的消息的时间戳和 message_id, + 否则还是指最新的消息的时间戳和 message_id。 + :param before_time: 查询起始的时间戳,返回小于这个时间(不包含)的记录,服务端默认是当前时间 + :param before_message_id: 起始的消息 id,使用时必须加上对应消息的时间 before_time 参数,一起作为查询的起点 + :return: 符合条件的聊天记录 + """ + query_params = {} # type: Dict[str, Any] + query_params["convid"] = conversation_id + if limit is not None: + query_params["limit"] = limit + if reversed is not None: + query_params["reversed"] = reversed + if isinstance(before_time, datetime): + query_params["max_ts"] = round(before_time.timestamp() * 1000) + elif isinstance(before_time, six.integer_types) or isinstance( + before_time, float + ): + query_params["max_ts"] = round(before_time * 1000) + if before_message_id is not None: + query_params["msgid"] = before_message_id + return list(cls._find(query_params)) + + @classmethod + def find_by_client( + cls, from_client, limit=None, before_time=None, before_message_id=None + ): + # type: (str, Optional[int], Optional[Union[datetime, float]], Optional[str]) -> List[Message] # noqa: E501 + """获取某个 client 的聊天记录 + + :param from_client: 要获取聊天记录的 client id + :param limit: 返回条数限制,可选,服务端默认 100 条,最大 1000 条 + :param before_time: 查询起始的时间戳,返回小于这个时间(不包含)的记录,服务端默认是当前时间 + :param before_message_id: 起始的消息 id,使用时必须加上对应消息的时间 before_time 参数,一起作为查询的起点 + :return: 符合条件的聊天记录 + """ + query_params = {} # type: Dict[str, Any] + query_params["from"] = from_client + if limit is not None: + query_params["limit"] = limit + if isinstance(before_time, datetime): + query_params["max_ts"] = round(before_time.timestamp() * 1000) + elif isinstance(before_time, six.integer_types) or isinstance( + before_time, float + ): + query_params["max_ts"] = round(before_time * 1000) + if before_message_id is not None: + query_params["msgid"] = before_message_id + return list(cls._find(query_params)) + + @classmethod + def find_all(cls, limit=None, before_time=None, before_message_id=None): + # type: (Optional[int], Optional[Union[datetime, float]], Optional[str]) -> List[Message] # noqa: E501 + """获取应用全部聊天记录 + + :param limit: 返回条数限制,可选,服务端默认 100 条,最大 1000 条 + :param before_time: 查询起始的时间戳,返回小于这个时间(不包含)的记录,服务端默认是当前时间 + :param before_message_id: 起始的消息 id,使用时必须加上对应消息的时间 before_time 参数,一起作为查询的起点 + :return: 符合条件的聊天记录 + """ + query_params = {} # type: Dict[str, Any] + if limit is not None: + query_params["limit"] = limit + if isinstance(before_time, datetime): + query_params["max_ts"] = round(before_time.timestamp() * 1000) + elif isinstance(before_time, six.integer_types) or isinstance( + before_time, float + ): + query_params["max_ts"] = round(before_time * 1000) + if before_message_id is not None: + query_params["msgid"] = before_message_id + return list(cls._find(query_params)) + + def _update_data(self, server_data): # type: (dict) -> None + self.bin = server_data.get("bin") + self.conversation_id = server_data.get("conv-id") + self.data = server_data.get("data") + self.from_client = server_data.get("from") + self.from_ip = server_data.get("from-ip") + self.is_conversation = server_data.get("is-conv") + self.is_room = server_data.get("is-room") + self.message_id = server_data.get("msg-id") + self.timestamp = server_data.get("timestamp", 0) / 1000 + self.to = server_data.get("to") diff --git a/leancloud/mime_type.py b/leancloud/mime_type.py deleted file mode 100644 index a9861ab4..00000000 --- a/leancloud/mime_type.py +++ /dev/null @@ -1,204 +0,0 @@ -# coding: utf-8 - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -__author__ = 'asaka ' - -mime_types = { - "ai": "application/postscript", - "aif": "audio/x-aiff", - "aifc": "audio/x-aiff", - "aiff": "audio/x-aiff", - "asc": "text/plain", - "atom": "application/atom+xml", - "au": "audio/basic", - "avi": "video/x-msvideo", - "bcpio": "application/x-bcpio", - "bin": "application/octet-stream", - "bmp": "image/bmp", - "cdf": "application/x-netcdf", - "cgm": "image/cgm", - "class": "application/octet-stream", - "cpio": "application/x-cpio", - "cpt": "application/mac-compactpro", - "csh": "application/x-csh", - "css": "text/css", - "dcr": "application/x-director", - "dif": "video/x-dv", - "dir": "application/x-director", - "djv": "image/vnd.djvu", - "djvu": "image/vnd.djvu", - "dll": "application/octet-stream", - "dmg": "application/octet-stream", - "dms": "application/octet-stream", - "doc": "application/msword", - "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml." + - "document", - "dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml." + - "template", - "docm": "application/vnd.ms-word.document.macroEnabled.12", - "dotm": "application/vnd.ms-word.template.macroEnabled.12", - "dtd": "application/xml-dtd", - "dv": "video/x-dv", - "dvi": "application/x-dvi", - "dxr": "application/x-director", - "eps": "application/postscript", - "etx": "text/x-setext", - "exe": "application/octet-stream", - "ez": "application/andrew-inset", - "gif": "image/gif", - "gram": "application/srgs", - "grxml": "application/srgs+xml", - "gtar": "application/x-gtar", - "hdf": "application/x-hdf", - "hqx": "application/mac-binhex40", - "htm": "text/html", - "html": "text/html", - "ice": "x-conference/x-cooltalk", - "ico": "image/x-icon", - "ics": "text/calendar", - "ief": "image/ief", - "ifb": "text/calendar", - "iges": "model/iges", - "igs": "model/iges", - "jnlp": "application/x-java-jnlp-file", - "jp2": "image/jp2", - "jpe": "image/jpeg", - "jpeg": "image/jpeg", - "jpg": "image/jpeg", - "js": "application/x-javascript", - "kar": "audio/midi", - "latex": "application/x-latex", - "lha": "application/octet-stream", - "lzh": "application/octet-stream", - "m3u": "audio/x-mpegurl", - "m4a": "audio/mp4a-latm", - "m4b": "audio/mp4a-latm", - "m4p": "audio/mp4a-latm", - "m4u": "video/vnd.mpegurl", - "m4v": "video/x-m4v", - "mac": "image/x-macpaint", - "man": "application/x-troff-man", - "mathml": "application/mathml+xml", - "me": "application/x-troff-me", - "mesh": "model/mesh", - "mid": "audio/midi", - "midi": "audio/midi", - "mif": "application/vnd.mif", - "mov": "video/quicktime", - "movie": "video/x-sgi-movie", - "mp2": "audio/mpeg", - "mp3": "audio/mpeg", - "mp4": "video/mp4", - "mpe": "video/mpeg", - "mpeg": "video/mpeg", - "mpg": "video/mpeg", - "mpga": "audio/mpeg", - "ms": "application/x-troff-ms", - "msh": "model/mesh", - "mxu": "video/vnd.mpegurl", - "nc": "application/x-netcdf", - "oda": "application/oda", - "ogg": "application/ogg", - "pbm": "image/x-portable-bitmap", - "pct": "image/pict", - "pdb": "chemical/x-pdb", - "pdf": "application/pdf", - "pgm": "image/x-portable-graymap", - "pgn": "application/x-chess-pgn", - "pic": "image/pict", - "pict": "image/pict", - "png": "image/png", - "pnm": "image/x-portable-anymap", - "pnt": "image/x-macpaint", - "pntg": "image/x-macpaint", - "ppm": "image/x-portable-pixmap", - "ppt": "application/vnd.ms-powerpoint", - "pptx": "application/vnd.openxmlformats-officedocument.presentationml." + - "presentation", - "potx": "application/vnd.openxmlformats-officedocument.presentationml." + - "template", - "ppsx": "application/vnd.openxmlformats-officedocument.presentationml." + - "slideshow", - "ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12", - "pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12", - "potm": "application/vnd.ms-powerpoint.template.macroEnabled.12", - "ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", - "ps": "application/postscript", - "qt": "video/quicktime", - "qti": "image/x-quicktime", - "qtif": "image/x-quicktime", - "ra": "audio/x-pn-realaudio", - "ram": "audio/x-pn-realaudio", - "ras": "image/x-cmu-raster", - "rdf": "application/rdf+xml", - "rgb": "image/x-rgb", - "rm": "application/vnd.rn-realmedia", - "roff": "application/x-troff", - "rtf": "text/rtf", - "rtx": "text/richtext", - "sgm": "text/sgml", - "sgml": "text/sgml", - "sh": "application/x-sh", - "shar": "application/x-shar", - "silo": "model/mesh", - "sit": "application/x-stuffit", - "skd": "application/x-koan", - "skm": "application/x-koan", - "skp": "application/x-koan", - "skt": "application/x-koan", - "smi": "application/smil", - "smil": "application/smil", - "snd": "audio/basic", - "so": "application/octet-stream", - "spl": "application/x-futuresplash", - "src": "application/x-wais-source", - "sv4cpio": "application/x-sv4cpio", - "sv4crc": "application/x-sv4crc", - "svg": "image/svg+xml", - "swf": "application/x-shockwave-flash", - "t": "application/x-troff", - "tar": "application/x-tar", - "tcl": "application/x-tcl", - "tex": "application/x-tex", - "texi": "application/x-texinfo", - "texinfo": "application/x-texinfo", - "tif": "image/tiff", - "tiff": "image/tiff", - "tr": "application/x-troff", - "tsv": "text/tab-separated-values", - "txt": "text/plain", - "ustar": "application/x-ustar", - "vcd": "application/x-cdlink", - "vrml": "model/vrml", - "vxml": "application/voicexml+xml", - "wav": "audio/x-wav", - "wbmp": "image/vnd.wap.wbmp", - "wbmxl": "application/vnd.wap.wbxml", - "wml": "text/vnd.wap.wml", - "wmlc": "application/vnd.wap.wmlc", - "wmls": "text/vnd.wap.wmlscript", - "wmlsc": "application/vnd.wap.wmlscriptc", - "wrl": "model/vrml", - "xbm": "image/x-xbitmap", - "xht": "application/xhtml+xml", - "xhtml": "application/xhtml+xml", - "xls": "application/vnd.ms-excel", - "xml": "application/xml", - "xpm": "image/x-xpixmap", - "xsl": "application/xml", - "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml." + - "template", - "xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12", - "xltm": "application/vnd.ms-excel.template.macroEnabled.12", - "xlam": "application/vnd.ms-excel.addin.macroEnabled.12", - "xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12", - "xslt": "application/xslt+xml", - "xul": "application/vnd.mozilla.xul+xml", - "xwd": "image/x-xwindowdump", - "xyz": "chemical/x-xyz", - "zip": "application/zip" -} diff --git a/leancloud/object_.py b/leancloud/object_.py index d7a5cf5a..920822ed 100644 --- a/leancloud/object_.py +++ b/leancloud/object_.py @@ -3,56 +3,55 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals -import time import copy import json -import warnings -import iso8601 -from werkzeug import LocalProxy +import six +from werkzeug.local import LocalProxy import leancloud from leancloud import utils from leancloud import client from leancloud import operation -from leancloud._compat import with_metaclass -from leancloud._compat import PY2 -from leancloud._compat import text_type -from leancloud._compat import iteritems -__author__ = 'asaka ' +__author__ = "asaka " object_class_map = {} class ObjectMeta(type): - def __new__(cls, name, bases, attrs): + def __new__(mcs, name, bases, attrs): cached_class = object_class_map.get(name) if cached_class: return cached_class - super_new = super(ObjectMeta, cls).__new__ + super_new = super(ObjectMeta, mcs).__new__ # let user define their class_name at subclass-creation stage - class_name = attrs.pop('class_name', None) + class_name = attrs.pop("class_name", None) if class_name: - attrs['_class_name'] = class_name - elif name == 'User': - attrs['_class_name'] = '_User' - elif name == 'Installation': - attrs['_class_name'] = '_Installation' - elif name == 'Notification': - attrs['_class_name'] = '_Notification' - elif name == 'Role': - attrs['_class_name'] = '_Role' + attrs["_class_name"] = class_name + elif name == "User": + attrs["_class_name"] = "_User" + elif name == "Installation": + attrs["_class_name"] = "_Installation" + elif name == "Notification": + attrs["_class_name"] = "_Notification" + elif name == "Role": + attrs["_class_name"] = "_Role" + elif name == "Conversation": + attrs["_class_name"] = "_Conversation" + elif name == "SysMessage": + attrs["_class_name"] = "_SysMessage" else: - attrs['_class_name'] = name + attrs["_class_name"] = name - object_class = super_new(cls, name, bases, attrs) + object_class = super_new(mcs, name, bases, attrs) object_class_map[name] = object_class return object_class @@ -66,7 +65,7 @@ def query(cls): return leancloud.Query(cls) -class Object(with_metaclass(ObjectMeta, object)): +class Object(six.with_metaclass(ObjectMeta, object)): def __init__(self, **attrs): """ 创建一个新的 leancloud.Object @@ -78,11 +77,11 @@ def __init__(self, **attrs): self._class_name = self._class_name # for IDE self._changes = {} self._attributes = {} + self._flags = {} self.created_at = None self.updated_at = None - self.fetch_when_save = False - for k, v in iteritems(attrs): + for k, v in six.iteritems(attrs): self.set(k, v) @classmethod @@ -95,8 +94,9 @@ def extend(cls, name): :return: 派生的子类 :rtype: ObjectMeta """ - if PY2 and isinstance(name, text_type): - name = name.encode('utf-8') + if six.PY2 and isinstance(name, six.text_type): + # In python2, class name must be a python2 str. + name = name.encode("utf-8") return type(name, (cls,), {}) @classmethod @@ -124,7 +124,7 @@ def create_without_data(cls, id_): :rtype: Object """ if cls is Object: - raise RuntimeError('can not call create_without_data on leancloud.Object') + raise RuntimeError("can not call create_without_data on leancloud.Object") obj = cls() obj.id = id_ return obj @@ -151,35 +151,53 @@ def destroy_all(cls, objs): """ if not objs: return - if not all(x._class_name == objs[0]._class_name for x in objs): - raise ValueError("destroy_all requires the argument object list's _class_names must be the same") if any(x.is_new() for x in objs): raise ValueError("Could not destroy unsaved object") - ids = {x.id for x in objs} - ids = ','.join(ids) - client.delete('/classes/{0}/{1}'.format(objs[0]._class_name, ids)) - @property - def attributes(self): - warnings.warn('leancloud.Object.attributes should not be used any more, please use get or set instead', leancloud.errors.LeanCloudWarning) - return self._attributes + dumped_objs = [] + for obj in objs: + dumped_obj = { + "method": "DELETE", + "path": "/{0}/classes/{1}/{2}".format( + client.SERVER_VERSION, obj._class_name, obj.id + ), + "body": obj._flags, + } + dumped_objs.append(dumped_obj) + + response = client.post("/batch", params={"requests": dumped_objs}).json() + + errors = [] + for idx in range(len(objs)): + content = response[idx] + error = content.get("error") + if error: + errors.append( + leancloud.LeanCloudError(error.get("code"), error.get("error")) + ) + + if errors: + # TODO: how to raise list of errors? + # raise MultipleValidationErrors(errors) + # add test + raise errors[0] def dump(self): obj = self._dump() - obj.pop('__type') - obj.pop('className') + obj.pop("__type") + obj.pop("className") return obj def _dump(self): obj = copy.deepcopy(self._attributes) - for k, v in iteritems(obj): + for k, v in six.iteritems(obj): obj[k] = utils.encode(v) if self.id is not None: - obj['objectId'] = self.id + obj["objectId"] = self.id - obj['__type'] = 'Object' - obj['className'] = self._class_name + obj["__type"] = "Object" + obj["className"] = self._class_name return obj def destroy(self): @@ -190,9 +208,9 @@ def destroy(self): """ if not self.id: return - client.delete('/classes/{0}/{1}'.format(self._class_name, self.id)) + client.delete("/classes/{0}/{1}".format(self._class_name, self.id), self._flags) - def save(self, where=None): + def save(self, where=None, fetch_when_save=None): """ 将对象数据保存至服务器 @@ -200,13 +218,17 @@ def save(self, where=None): :rtype: None """ if where and not isinstance(where, leancloud.Query): - raise TypeError('where param type should be leancloud.Query, got %s', type(where)) + raise TypeError( + "where param type should be leancloud.Query, got %s", type(where) + ) if where and where._query_class._class_name != self._class_name: - raise TypeError('where param\'s class name not equal to the current object\'s class name') + raise TypeError( + "where param's class name not equal to the current object's class name" + ) if where and self.is_new(): - raise TypeError('where params works only when leancloud.Object is saved') + raise TypeError("where params works only when leancloud.Object is saved") unsaved_children = [] unsaved_files = [] @@ -215,14 +237,23 @@ def save(self, where=None): self._deep_save(unsaved_children, unsaved_files, exclude=self._attributes) data = self._dump_save() - fetch_when_save = 'true' if self.fetch_when_save else 'false' + fetch_when_save = "true" if fetch_when_save else "false" if self.is_new(): - response = client.post('/classes/{0}?fetchWhenSave={1}'.format(self._class_name, fetch_when_save), data) + response = client.post( + "/classes/{0}?fetchWhenSave={1}".format( + self._class_name, fetch_when_save + ), + data, + ) else: - url = '/classes/{0}/{1}?fetchWhenSave={2}'.format(self._class_name, self.id, fetch_when_save) + url = "/classes/{0}/{1}?fetchWhenSave={2}".format( + self._class_name, self.id, fetch_when_save + ) if where: - url += '&where=' + json.dumps(where.dump()['where'], separators=(',', ':')) + url += "&where=" + json.dumps( + where.dump()["where"], separators=(",", ":") + ) response = client.put(url, data) self._update_data(response.json()) @@ -238,35 +269,43 @@ def _deep_save(self, unsaved_children, unsaved_files, exclude=None): return dumped_objs = [] for obj in unsaved_children: - method = 'POST' if obj.id is None else 'PUT' - path = '/{0}/classes/{1}'.format(client.SERVER_VERSION, obj._class_name) + if obj.id is None: + method = "POST" + path = "/{0}/classes/{1}".format(client.SERVER_VERSION, obj._class_name) + else: + method = "PUT" + path = "/{0}/classes/{1}/{2}".format( + client.SERVER_VERSION, obj._class_name, obj.id + ) body = obj._dump_save() dumped_obj = { - 'method': method, - 'path': path, - 'body': body, + "method": method, + "path": path, + "body": body, } dumped_objs.append(dumped_obj) - response = client.post('/batch', params={'requests': dumped_objs}).json() + response = client.post("/batch", params={"requests": dumped_objs}).json() errors = [] for idx, obj in enumerate(unsaved_children): content = response[idx] - if not content.get('success'): - errors.append(leancloud.LeanCloudError(content.get('code'), content.get('error'))) + error = content.get("error") + if error: + errors.append( + leancloud.LeanCloudError(error.get("code"), error.get("error")) + ) else: - obj._update_data(content['success']) + obj._update_data(content["success"]) - if errors: - # TODO: how to raise list of errors? - # raise MultipleValidationErrors(errors) - # add test - raise errors[0] + if errors: + # TODO: how to raise list of errors? + # raise MultipleValidationErrors(errors) + # add test + raise errors[0] @classmethod def _find_unsaved_children(cls, obj, children, files): - def callback(o): if isinstance(o, Object): if o.is_dirty(): @@ -281,7 +320,7 @@ def callback(o): utils.traverse_object(obj, callback) def is_dirty(self, attr=None): - #consider renaming to is_changed? + # consider renaming to is_changed? if attr: return attr in self._changes else: @@ -289,31 +328,31 @@ def is_dirty(self, attr=None): def _to_pointer(self): return { - '__type': 'Pointer', - 'className': self._class_name, - 'objectId': self.id, + "__type": "Pointer", + "className": self._class_name, + "objectId": self.id, } def _merge_metadata(self, server_data): - for key in ('objectId', 'createdAt', 'updatedAt'): - if server_data.get(key) is None: - continue - if key == 'objectId': - self.id = server_data[key] - else: - dt = iso8601.parse_date(server_data[key]) - if key == 'createdAt': - self.created_at = dt - else: - self.updated_at = dt - del server_data[key] + object_id = server_data.get("objectId") + _created_at = utils.decode_date_string(server_data.get("createdAt")) + _updated_at = utils.decode_updated_at(server_data.get("updatedAt"), _created_at) + + if object_id is not None: + self.id = object_id + if _created_at is not None: + self.created_at = _created_at + if _updated_at is not None: + self.updated_at = _updated_at + + def validate(self, attrs): - if 'ACL' in attrs and not isinstance(attrs['ACL'], leancloud.ACL): - raise TypeError('acl must be a ACL') + if "ACL" in attrs and not isinstance(attrs["ACL"], leancloud.ACL): + raise TypeError("acl must be a ACL") return True - def get(self, attr, deafult=None): + def get(self, attr, default=None, deafult=None): """ 获取对象字段的值 @@ -321,7 +360,27 @@ def get(self, attr, deafult=None): :type attr: string_types :return: 字段值 """ - return self._attributes.get(attr, deafult) + # for backward compatibility + if (deafult is not None) and (default is None): + default = deafult + + # createdAt is stored as string in the cloud but used as datetime object on the client side. + # We need to make sure that `.created_at` and `.get("createdAt")` return the same value. + # Otherwise users will get confused. + if attr == "createdAt": + if self.created_at is None: + return None + else: + return self.created_at + + # Similar to createdAt. + if attr == "updatedAt": + if self.updated_at is None: + return None + else: + return self.updated_at + + return self._attributes.get(attr, default) def relation(self, attr): """ @@ -335,7 +394,7 @@ def relation(self, attr): value = self.get(attr) if value is not None: if not isinstance(value, leancloud.Relation): - raise TypeError('field %s is not Relation'.format(attr)) + raise TypeError("field %s is not Relation".format(attr)) value._ensure_parent_and_key(self, attr) return value return leancloud.Relation(self, attr) @@ -388,7 +447,7 @@ def set(self, key_or_attrs, value=None, unset=False): if not isinstance(v, operation.BaseOp): v = operation.Set(v) - self._attributes[k] = v._apply(self._attributes.get(k),self, k) + self._attributes[k] = v._apply(self._attributes.get(k), self, k) if self._attributes[k] == operation._UNSET: del self._attributes[k] self._changes[k] = v._merge(self._changes.get(k)) @@ -444,6 +503,15 @@ def remove(self, attr, item): """ return self.set(attr, operation.Remove([item])) + def bit_and(self, attr, value): + return self.set(attr, operation.BitAnd(value)) + + def bit_or(self, attr, value): + return self.set(attr, operation.BitOr(value)) + + def bit_xor(self, attr, value): + return self.set(attr, operation.BitXor(value)) + def clear(self): """ 将当前对象所有字段全部移除。 @@ -453,27 +521,49 @@ def clear(self): self.set(self._attributes, unset=True) def _dump_save(self): - return {k:v.dump() for k,v in iteritems(self._changes)} + data = {k: v.dump() for k, v in six.iteritems(self._changes)} + data.update(self._flags) + return data - def fetch(self): + def fetch(self, select=None, include=None): """ 从服务器获取当前对象所有的值,如果与本地值不同,将会覆盖本地的值。 :return: 当前对象 """ - response = client.get('/classes/{0}/{1}'.format(self._class_name, self.id), {}) + data = {} + if select: + if not isinstance(select, (list, tuple)): + raise TypeError("select parameter must be a list or a tuple") + data["keys"] = ",".join(select) + if include: + if not isinstance(include, (list, tuple)): + raise TypeError("include parameter must be a list or a tuple") + data["include"] = ",".join(include) + response = client.get( + "/classes/{0}/{1}".format(self._class_name, self.id), data + ) self._update_data(response.json()) def is_new(self): """ 判断当前对象是否已经保存至服务器。 + 该方法为 SDK 内部使用(save 调用此方法 dispatch 保存操作为 REST API 的 POST 和 PUT 请求)。 + 查询对象是否在服务器上存在请使用 is_existed 方法。 + + :rtype: bool """ return False if self.id else True def is_existed(self): - return bool(self.id) + """ + 判断当前对象是否在服务器上已经存在。 + + :rtype: bool + """ + return self.has("createdAt") def get_acl(self): """ @@ -482,7 +572,7 @@ def get_acl(self): :return: 当前对象的 ACL :rtype: leancloud.ACL """ - return self.get('ACL') + return self.get("ACL") def set_acl(self, acl): """ @@ -492,26 +582,47 @@ def set_acl(self, acl): :return: 当前对象 """ - return self.set('ACL', acl) + return self.set("ACL", acl) def disable_before_hook(self): - master_key = client.get_app_info().get('master_key') - if not master_key: - raise ValueError('disable_before_hook need LeanCloud master key') - timestamp = int(time.time() * 1000) - return self.set('__before', utils.sign_disable_hook('__before_for_' + self._class_name, master_key, timestamp)) + hook_key = client.get_app_info().get("hook_key") + master_key = client.get_app_info().get("master_key") + if hook_key or master_key: + self.ignore_hook("beforeSave") + self.ignore_hook("beforeUpdate") + self.ignore_hook("beforeDelete") + return self + else: + raise ValueError("disable_before_hook needs master key or hook key") def disable_after_hook(self): - master_key = client.get_app_info().get('master_key') - if not master_key: - raise ValueError('disable_before_hook need LeanCloud master key') - timestamp = int(time.time() * 1000) - return self.set('__after', utils.sign_disable_hook('__after_for_' + self._class_name, master_key, timestamp)) + hook_key = client.get_app_info().get("hook_key") + master_key = client.get_app_info().get("master_key") + if hook_key or master_key: + self.ignore_hook("afterSave") + self.ignore_hook("afterUpdate") + self.ignore_hook("afterDelete") + return self + else: + raise ValueError("disable_after_hook needs master key or hook key") + + def ignore_hook(self, hook_name): + if hook_name not in { + "beforeSave", + "afterSave", + "beforeUpdate", + "afterUpdate", + "beforeDelete", + "afterDelete", + }: + raise ValueError("invalid hook name: " + hook_name) + if "__ignore_hooks" not in self._flags: + self._flags["__ignore_hooks"] = [] + self._flags["__ignore_hooks"].append(hook_name) def _update_data(self, server_data): - self._merge_metadata(server_data) - for key, value in iteritems(server_data): + for key, value in six.iteritems(server_data): self._attributes[key] = utils.decode(key, value) self._changes = {} @@ -520,4 +631,5 @@ def as_class(arg): def inner_decorator(cls): cls._class_name = arg return cls + return inner_decorator diff --git a/leancloud/operation.py b/leancloud/operation.py index 83b878de..fd0d7c79 100644 --- a/leancloud/operation.py +++ b/leancloud/operation.py @@ -3,13 +3,14 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import copy import leancloud import leancloud.utils -__author__ = 'asaka ' +__author__ = "asaka " class BaseOp(object): @@ -49,9 +50,7 @@ def __init__(self): pass def dump(self): - return { - '__op': 'Delete' - } + return {"__op": "Delete"} def _merge(self, previous): return self @@ -70,8 +69,8 @@ def amount(self): def dump(self): return { - '__op': 'Increment', - 'amount': self.amount, + "__op": "Increment", + "amount": self.amount, } def _merge(self, previous): @@ -83,7 +82,7 @@ def _merge(self, previous): return Set(previous.value + self.amount) elif isinstance(previous, Increment): return Increment(self.amount + previous.amount) - raise TypeError('invalid op') + raise TypeError("invalid op") def _apply(self, old, obj=None, key=None): if not old: @@ -91,10 +90,91 @@ def _apply(self, old, obj=None, key=None): return old + self.amount +class BitAnd(BaseOp): + def __init__(self, value): + self._value = value + + @property + def value(self): + return self._value + + def dump(self): + return { + "__op": "BitAnd", + "value": self.value, + } + + def _merge(self, previous): + if not previous: + return self + if isinstance(previous, Unset): + return Set(0) + if isinstance(previous, Set): + return Set(previous.value & self.value) + raise TypeError("invalid op") + + def _apply(self, old, obj=None, key=None): + return old & self.value + + +class BitOr(BaseOp): + def __init__(self, value): + self._value = value + + @property + def value(self): + return self._value + + def dump(self): + return { + "__op": "BitOr", + "value": self.value, + } + + def _merge(self, previous): + if not previous: + return self + if isinstance(previous, Unset): + return Set(self.value) + if isinstance(previous, Set): + return Set(previous.value | self.value) + raise TypeError("invalid op") + + def _apply(self, old, obj=None, key=None): + return old | self.value + + +class BitXor(BaseOp): + def __init__(self, value): + self._value = value + + @property + def value(self): + return self._value + + def dump(self): + return { + "__op": "BitXor", + "value": self.value, + } + + def _merge(self, previous): + if not previous: + return self + if isinstance(previous, Unset): + return Set(self.value) + if isinstance(previous, Set): + return Set(previous.value ^ self.value) + raise TypeError("invalid op") + + def _apply(self, old, obj=None, key=None): + return old ^ self.value + + class Add(BaseOp): def __init__(self, objects): if not isinstance(objects, (list, tuple)): - raise TypeError('Add op requires list or tuple as parameters') + raise TypeError("Add op requires list or tuple as parameters") self._objects = objects @property @@ -103,8 +183,8 @@ def objects(self): def dump(self): return { - '__op': 'Add', - 'objects': leancloud.utils.encode(self.objects), + "__op": "Add", + "objects": leancloud.utils.encode(self.objects), } def _merge(self, previous): @@ -116,7 +196,7 @@ def _merge(self, previous): return Set(self._apply(previous.value)) elif isinstance(previous, Add): return Add(previous.objects + self.objects) - raise TypeError('invalid op') + raise TypeError("invalid op") def _apply(self, old, obj=None, key=None): if not old: @@ -127,7 +207,7 @@ def _apply(self, old, obj=None, key=None): class AddUnique(BaseOp): def __init__(self, objects): if not isinstance(objects, (list, tuple)): - raise TypeError('op.AddUnique requires list or tuple as parameters') + raise TypeError("op.AddUnique requires list or tuple as parameters") self._objects = list(set(objects)) @property @@ -136,8 +216,8 @@ def objects(self): def dump(self): return { - '__op': 'AddUnique', - 'objects': leancloud.utils.encode(self.objects), + "__op": "AddUnique", + "objects": leancloud.utils.encode(self.objects), } def _merge(self, previous): @@ -149,7 +229,7 @@ def _merge(self, previous): return Set(self._apply(previous.value)) elif isinstance(previous, AddUnique): return AddUnique(self._apply(previous.objects)) - raise TypeError('invalid op') + raise TypeError("invalid op") def _apply(self, old, obj=None, key=None): if not old: @@ -172,7 +252,7 @@ def _apply(self, old, obj=None, key=None): class Remove(BaseOp): def __init__(self, objects): if not isinstance(objects, (list, tuple)): - raise TypeError('op.Remove requires list or tuple as parameters') + raise TypeError("op.Remove requires list or tuple as parameters") self._objects = list(set(objects)) @property @@ -180,10 +260,7 @@ def objects(self): return self._objects def dump(self): - return { - '__op': 'Remove', - 'objects': leancloud.utils.encode(self.objects) - } + return {"__op": "Remove", "objects": leancloud.utils.encode(self.objects)} def _merge(self, previous): if not previous: @@ -194,7 +271,7 @@ def _merge(self, previous): return Set(self._apply(previous.value)) elif isinstance(previous, Remove): return Remove(list(set(self.objects + previous.objects))) - raise TypeError('invalid op') + raise TypeError("invalid op") def _apply(self, old, obj=None, key=None): if not old: @@ -202,7 +279,11 @@ def _apply(self, old, obj=None, key=None): new = list(set(old) - set(self.objects)) for obj in self.objects: if isinstance(obj, leancloud.Object) and obj.id: - new = [x for x in new if not (isinstance(x, leancloud.Object) and x.id == obj.id)] + new = [ + x + for x in new + if not (isinstance(x, leancloud.Object) and x.id == obj.id) + ] return new @@ -216,11 +297,11 @@ def __init__(self, adds, removes): def _pointer_to_id(self, obj): if isinstance(obj, leancloud.Object): if obj.id is None: - raise TypeError('cant add an unsaved Object to a relation') + raise TypeError("cant add an unsaved Object to a relation") if self._target_class_name is None: self._target_class_name = obj._class_name if self._target_class_name != obj._class_name: - raise TypeError('try to create a Relation with 2 different types') + raise TypeError("try to create a Relation with 2 different types") return obj.id return obj @@ -248,26 +329,24 @@ def dump(self): def id_to_pointer(id_): return { - '__type': 'Pointer', - 'className': self._target_class_name, - 'objectId': id_, + "__type": "Pointer", + "className": self._target_class_name, + "objectId": id_, } + if len(self.relations_to_add) > 0: adds = { - '__op': 'AddRelation', - 'objects': [id_to_pointer(x) for x in self.relations_to_add] + "__op": "AddRelation", + "objects": [id_to_pointer(x) for x in self.relations_to_add], } if len(self.relations_to_remove) > 0: removes = { - '__op': 'RemoveRelation', - 'objects': [id_to_pointer(x) for x in self.relations_to_remove] + "__op": "RemoveRelation", + "objects": [id_to_pointer(x) for x in self.relations_to_remove], } if adds and removes: - return { - '__op': 'Batch', - 'ops': [adds, removes] - } + return {"__op": "Batch", "ops": [adds, removes]} return adds or removes or {} @@ -275,18 +354,28 @@ def _merge(self, previous=None): if previous is None: return self elif isinstance(previous, Unset): - raise ValueError('can\'t modify a relation after deleting it.') + raise ValueError("can't modify a relation after deleting it.") elif isinstance(previous, Relation): - if (previous._target_class_name) and (previous._target_class_name != self._target_class_name): - raise TypeError('related object must be class of {0}'.format(previous._target_class_name)) - new_add = (previous.relations_to_add - self.relations_to_remove) | self.relations_to_add - new_remove = (previous.relations_to_remove - self.relations_to_add) | self.relations_to_remove + if (previous._target_class_name) and ( + previous._target_class_name != self._target_class_name + ): + raise TypeError( + "related object must be class of {0}".format( + previous._target_class_name + ) + ) + new_add = ( + previous.relations_to_add - self.relations_to_remove + ) | self.relations_to_add + new_remove = ( + previous.relations_to_remove - self.relations_to_add + ) | self.relations_to_remove new_relation = Relation(new_add, new_remove) new_relation._target_class_name = self._target_class_name return new_relation else: - raise TypeError('invalid op') + raise TypeError("invalid op") def _apply(self, old, obj=None, key=None): if old is None: @@ -295,9 +384,13 @@ def _apply(self, old, obj=None, key=None): if self._target_class_name: if old.target_class_name: if old.target_class_name != self._target_class_name: - raise TypeError('related object must be class of {0}'.format(old.target_class_name)) + raise TypeError( + "related object must be class of {0}".format( + old.target_class_name + ) + ) else: old.target_class_name = self._target_class_name return old else: - raise TypeError('invalid op') + raise TypeError("invalid op") diff --git a/leancloud/push.py b/leancloud/push.py index 8cf8ca51..5b8f5d9a 100644 --- a/leancloud/push.py +++ b/leancloud/push.py @@ -3,15 +3,17 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import arrow -from dateutil import tz +import dateutil.tz as tz from leancloud.object_ import Object +from leancloud.errors import LeanCloudError from leancloud import client -__author__ = 'asaka ' +__author__ = "asaka " class Installation(Object): @@ -19,10 +21,34 @@ class Installation(Object): class Notification(Object): - pass + def fetch(self, *args, **kwargs): + """同步服务器的 Notification 数据 + """ + response = client.get("/tables/Notifications/{0}".format(self.id)) + self._update_data(response.json()) + + def save(self, *args, **kwargs): + raise LeanCloudError(code=1, error="Notification does not support modify") + + +def _encode_time(time): + tzinfo = time.tzinfo + if tzinfo is None: + tzinfo = tz.tzlocal() + return arrow.get(time, tzinfo).to("utc").format("YYYY-MM-DDTHH:mm:ss.SSS") + "Z" -def send(data, channels=None, push_time=None, expiration_time=None, expiration_interval=None, where=None, cql=None): +def send( + data, + channels=None, + push_time=None, + expiration_time=None, + expiration_interval=None, + where=None, + cql=None, + flow_control=None, + prod=None, +): """ 发送推送消息。返回结果为此条推送对应的 _Notification 表中的对象,但是如果需要使用其中的数据,需要调用 fetch() 方法将数据同步至本地。 @@ -40,29 +66,43 @@ def send(data, channels=None, push_time=None, expiration_time=None, expiration_i :type cql: string_types :param data: 推送给设备的具体信息,详情查看 https://leancloud.cn/docs/push_guide.html#消息内容_Data :rtype: Notification + :param flow_control: 不为 None 时开启平滑推送,值为每秒推送的目标终端用户数。开启时指定低于 1000 的值,按 1000 计。 + :type: flow_control: int + :param prod: 仅对 iOS 推送有效,设置将推送发至 APNs 的开发环境(dev)还是生产环境(prod)。 + :type: prod: string """ - if push_time and expiration_time: - raise TypeError('Both expiration_time and expiration_time_interval can\'t be set') + if expiration_interval and expiration_time: + raise TypeError("Both expiration_time and expiration_interval can't be set") + params = { - 'data': data, + "data": data, } + + if prod is None: + if client.USE_PRODUCTION == "0": + params["prod"] = "dev" + else: + params["prod"] = prod + if channels: - params['channels'] = channels + params["channels"] = channels if push_time: - tzinfo = push_time.tzinfo - if tzinfo is None: - tzinfo = tz.tzlocal() - params['push_time'] = arrow.get(push_time, tzinfo).to('utc').format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z' + params["push_time"] = _encode_time(push_time) if expiration_time: - params['expiration_time'] = expiration_time.isoformat() + params["expiration_time"] = _encode_time(expiration_time) if expiration_interval: - params['expiration_interval'] = expiration_interval + params["expiration_interval"] = expiration_interval if where: - params['where'] = where.dump().get('where', {}) + params["where"] = where.dump().get("where", {}) if cql: - params['cql'] = cql + params["cql"] = cql + # Do not change this to `if flow_control`, because 0 is falsy in Python, + # but `flow_control = 0` will enable smooth push, + # and it is in fact equivalent to `flow_control = 1000`. + if flow_control is not None: + params["flow_control"] = flow_control - result = client.post('/push', params=params).json() + result = client.post("/push", params=params).json() - notification = Notification.create_without_data(result['objectId']) + notification = Notification.create_without_data(result["objectId"]) return notification diff --git a/leancloud/query.py b/leancloud/query.py index 72a9a53d..e1ab1999 100644 --- a/leancloud/query.py +++ b/leancloud/query.py @@ -3,20 +3,20 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import json -import warnings + +import six import leancloud from leancloud import client from leancloud import utils +from leancloud.file_ import File from leancloud.object_ import Object from leancloud.errors import LeanCloudError -from leancloud.errors import LeanCloudWarning -from leancloud._compat import string_types -from leancloud._compat import class_types -__author__ = 'asaka ' +__author__ = "asaka " class CQLResult(object): @@ -30,12 +30,46 @@ class CQLResult(object): class_name: 查询的 class 名称 """ + + __slots__ = ["results", "count", "class_name"] + def __init__(self, results, count, class_name): self.results = results self.count = count self.class_name = class_name +class Cursor(object): + """ + Query.scan 返回结果对象。 + """ + + def __init__(self, query_class, batch_size, scan_key, params): + self._params = params + self._query_class = query_class + + if batch_size is not None: + self._params["limit"] = batch_size + + if scan_key is not None: + self._params["scan_key"] = scan_key + + def __iter__(self): + while True: + content = client.get( + "/scan/classes/{}".format(self._query_class._class_name), self._params + ).json() + for result in content["results"]: + obj = self._query_class() + obj._update_data(result) + yield obj + + if not content.get("cursor"): + break + + self._params["cursor"] = content["cursor"] + + class Query(object): def __init__(self, query_class): """ @@ -43,26 +77,28 @@ def __init__(self, query_class): :param query_class: 要查询的 class 名称或者对象 :type query_class: string_types or leancloud.ObjectMeta """ - if isinstance(query_class, string_types): - query_class = Object.extend(query_class) + if isinstance(query_class, six.string_types): + if query_class in ("File", "_File"): + query_class = File + else: + query_class = Object.extend(query_class) - if (not isinstance(query_class, (type, class_types))) or (not issubclass(query_class, Object)): - raise ValueError('Query takes string or LeanCloud Object') + if not isinstance(query_class, (type, six.class_types)) or not issubclass( + query_class, (File, Object) + ): + raise ValueError("Query takes string or LeanCloud Object") self._query_class = query_class self._where = {} self._include = [] + self._include_acl = None self._limit = -1 self._skip = 0 self._extra = {} self._order = [] self._select = [] - def __call__(self): - warnings.warn('leancloud.Relation.query now is a property, please don\'t call it as a function', LeanCloudWarning) - return self - @classmethod def or_(cls, *queries): """ @@ -72,9 +108,12 @@ def or_(cls, *queries): :rtype: Query """ if len(queries) < 2: - raise ValueError('or_ need two queries at least') - if not all(x._query_class._class_name == queries[0]._query_class._class_name for x in queries): - raise TypeError('All queries must be for the same class') + raise ValueError("or_ need two queries at least") + if not all( + x._query_class._class_name == queries[0]._query_class._class_name + for x in queries + ): + raise TypeError("All queries must be for the same class") query = Query(queries[0]._query_class._class_name) query._or_query(queries) return query @@ -88,9 +127,12 @@ def and_(cls, *queries): :rtype: Query """ if len(queries) < 2: - raise ValueError('and_ need two queries at least') - if not all(x._query_class._class_name == queries[0]._query_class._class_name for x in queries): - raise TypeError('All queries must be for the same class') + raise ValueError("and_ need two queries at least") + if not all( + x._query_class._class_name == queries[0]._query_class._class_name + for x in queries + ): + raise TypeError("All queries must be for the same class") query = Query(queries[0]._query_class._class_name) query._and_query(queries) return query @@ -104,22 +146,22 @@ def do_cloud_query(cls, cql, *pvalues): :param pvalues: 查询参数 :rtype: CQLResult """ - params = {'cql': cql} + params = {"cql": cql} if len(pvalues) == 1 and isinstance(pvalues[0], (tuple, list)): pvalues = json.dumps(pvalues[0]) if len(pvalues) > 0: - params['pvalues'] = json.dumps(pvalues) + params["pvalues"] = json.dumps(pvalues) - content = client.get('/cloudQuery', params).json() + content = client.get("/cloudQuery", params).json() objs = [] - query = cls(content['className']) - for result in content['results']: + query = cls(content["className"]) + for result in content["results"]: obj = query._new_object() obj._update_data(query._process_result(result)) objs.append(obj) - return CQLResult(objs, content.get('count'), content.get('className')) + return CQLResult(objs, content.get("count"), content.get("className")) def dump(self): """ @@ -127,18 +169,20 @@ def dump(self): :rtype: dict """ params = { - 'where': self._where, + "where": self._where, } if self._include: - params['include'] = ','.join(self._include) + params["include"] = ",".join(self._include) if self._select: - params['keys'] = ','.join(self._select) + params["keys"] = ",".join(self._select) + if self._include_acl is not None: + params["returnACL"] = json.dumps(self._include_acl) if self._limit >= 0: - params['limit'] = self._limit + params["limit"] = self._limit if self._skip > 0: - params['skip'] = self._skip + params["skip"] = self._skip if self._order: - params['order'] = ",".join(self._order) + params["order"] = ",".join(self._order) params.update(self._extra) return params @@ -148,6 +192,11 @@ def _new_object(self): def _process_result(self, obj): return obj + def _do_request(self, params): + return client.get( + "/classes/{0}".format(self._query_class._class_name), params + ).json() + def first(self): """ 根据查询获取最多一个对象。 @@ -157,11 +206,11 @@ def first(self): :raise: LeanCloudError """ params = self.dump() - params['limit'] = 1 - content = client.get('/classes/{0}'.format(self._query_class._class_name), params).json() - results = content['results'] + params["limit"] = 1 + content = self._do_request(params) + results = content["results"] if not results: - raise LeanCloudError(101, 'Object not found') + raise LeanCloudError(101, "Object not found") obj = self._new_object() obj._update_data(self._process_result(results[0])) return obj @@ -174,8 +223,11 @@ def get(self, object_id): :return: 查询结果 :rtype: Object """ - self.equal_to('objectId', object_id) - return self.first() + if not object_id: + raise LeanCloudError(code=101, error="Object not found.") + obj = self._query_class.create_without_data(object_id) + obj.fetch(select=self._select, include=self._include) + return obj def find(self): """ @@ -183,24 +235,23 @@ def find(self): :rtype: list """ - content = client.get('/classes/{0}'.format(self._query_class._class_name), self.dump()).json() + content = self._do_request(self.dump()) objs = [] - for result in content['results']: + for result in content["results"]: obj = self._new_object() obj._update_data(self._process_result(result)) objs.append(obj) return objs - # def destroy_all(self): - # """ - # 在服务器上删除所有满足查询条件的对象。 - - # :raise: LeanCLoudError - # """ - # result = client.delete('/classes/{0}'.format(self._query_class._class_name), self.dump()) - # return result + def scan(self, batch_size=None, scan_key=None): + params = self.dump() + if "skip" in params: + raise LeanCloudError(1, "Query.scan dose not support skip option") + if "limit" in params: + raise LeanCloudError(1, "Query.scan dose not support limit option") + return Cursor(self._query_class, batch_size, scan_key, params) def count(self): """ @@ -209,10 +260,10 @@ def count(self): :rtype: int """ params = self.dump() - params['limit'] = 0 - params['count'] = 1 - response = client.get('/classes/{0}'.format(self._query_class._class_name), params) - return response.json()['count'] + params["limit"] = 0 + params["count"] = 1 + content = self._do_request(params) + return content["count"] def skip(self, n): """ @@ -232,10 +283,21 @@ def limit(self, n): :rtype: Query """ if n > 1000: - raise ValueError('limit only accept number less than or equal to 1000') + raise ValueError("limit only accept number less than or equal to 1000") self._limit = n return self + def include_acl(self, value=True): + """ + 设置查询结果的对象,是否包含 ACL 字段。需要在控制台选项中开启对应选项才能生效。 + + :param value: 是否包含 ACL,默认为 True + :type value: bool + :rtype: Query + """ + self._include_acl = value + return self + def equal_to(self, key, value): """ 增加查询条件,查询字段的值必须为指定值。 @@ -247,6 +309,17 @@ def equal_to(self, key, value): self._where[key] = utils.encode(value) return self + def size_equal_to(self, key, size): + """ + 增加查询条件,限制查询结果指定数组字段长度与查询值相同 + + :param key: 查询条件数组字段名 + :param size: 查询条件值 + :rtype: Query + """ + self._add_condition(key, "$size", size) + return self + def _add_condition(self, key, condition, value): if not self._where.get(key): self._where[key] = {} @@ -261,7 +334,7 @@ def not_equal_to(self, key, value): :param value: 查询条件值 :rtype: Query """ - self._add_condition(key, '$ne', value) + self._add_condition(key, "$ne", value) return self def less_than(self, key, value): @@ -272,7 +345,7 @@ def less_than(self, key, value): :param value: 查询条件值 :rtype: Query """ - self._add_condition(key, '$lt', value) + self._add_condition(key, "$lt", value) return self def greater_than(self, key, value): @@ -283,7 +356,7 @@ def greater_than(self, key, value): :param value: 查询条件值 :rtype: Query """ - self._add_condition(key, '$gt', value) + self._add_condition(key, "$gt", value) return self def less_than_or_equal_to(self, key, value): @@ -294,7 +367,7 @@ def less_than_or_equal_to(self, key, value): :param value: 查询条件值 :rtype: Query """ - self._add_condition(key, '$lte', value) + self._add_condition(key, "$lte", value) return self def greater_than_or_equal_to(self, key, value): @@ -305,7 +378,7 @@ def greater_than_or_equal_to(self, key, value): :param value: 查询条件值名 :rtype: Query """ - self._add_condition(key, '$gte', value) + self._add_condition(key, "$gte", value) return self def contained_in(self, key, values): @@ -317,7 +390,7 @@ def contained_in(self, key, values): :type values: list or tuple :rtype: Query """ - self._add_condition(key, '$in', values) + self._add_condition(key, "$in", values) return self def not_contained_in(self, key, values): @@ -329,7 +402,7 @@ def not_contained_in(self, key, values): :type values: list or tuple :rtype: Query """ - self._add_condition(key, '$nin', values) + self._add_condition(key, "$nin", values) return self def contains_all(self, key, values): @@ -341,7 +414,7 @@ def contains_all(self, key, values): :type values: list or tuple :rtype: Query """ - self._add_condition(key, '$all', values) + self._add_condition(key, "$all", values) return self def exists(self, key): @@ -351,17 +424,17 @@ def exists(self, key): :param key: 查询条件字段名 :rtype: Query """ - self._add_condition(key, '$exists', True) + self._add_condition(key, "$exists", True) return self - def does_not_exists(self, key): + def does_not_exist(self, key): """ 增加查询条件,限制查询结果对象不包含指定字段 :param key: 查询条件字段名 :rtype: Query """ - self._add_condition(key, '$exists', False) + self._add_condition(key, "$exists", False) return self def matched(self, key, regex, ignore_case=False, multi_line=False): @@ -374,16 +447,16 @@ def matched(self, key, regex, ignore_case=False, multi_line=False): :param multi_line: 查询是否匹配多行,默认不匹配 :rtype: Query """ - if not isinstance(regex, string_types): - raise TypeError('matched only accept str or unicode') - self._add_condition(key, '$regex', regex) - modifiers = '' + if not isinstance(regex, six.string_types): + raise TypeError("matched only accept str or unicode") + self._add_condition(key, "$regex", regex) + modifiers = "" if ignore_case: - modifiers += 'i' + modifiers += "i" if multi_line: - modifiers += 'm' + modifiers += "m" if modifiers: - self._add_condition(key, '$options', modifiers) + self._add_condition(key, "$options", modifiers) return self def matches_query(self, key, query): @@ -396,8 +469,8 @@ def matches_query(self, key, query): :rtype: Query """ dumped = query.dump() - dumped['className'] = query._query_class._class_name - self._add_condition(key, '$inQuery', dumped) + dumped["className"] = query._query_class._class_name + self._add_condition(key, "$inQuery", dumped) return self def does_not_match_query(self, key, query): @@ -410,14 +483,10 @@ def does_not_match_query(self, key, query): :rtype: Query """ dumped = query.dump() - dumped['className'] = query._query_class._class_name - self._add_condition(key, '$notInQuery', dumped) + dumped["className"] = query._query_class._class_name + self._add_condition(key, "$notInQuery", dumped) return self - def matched_key_in_query(self, key, query_key, query): - warnings.warn(' the query is deprecated, please use matches_key_in_query', LeanCloudWarning) - return self.matches_key_in_query(key, query_key, query) - def matches_key_in_query(self, key, query_key, query): """ 增加查询条件,限制查询结果对象指定字段的值,与另外一个查询对象的返回结果指定的值相同。 @@ -429,8 +498,8 @@ def matches_key_in_query(self, key, query_key, query): :rtype: Query """ dumped = query.dump() - dumped['className'] = query._query_class._class_name - self._add_condition(key, '$select', {'key': query_key, 'query': dumped}) + dumped["className"] = query._query_class._class_name + self._add_condition(key, "$select", {"key": query_key, "query": dumped}) return self def does_not_match_key_in_query(self, key, query_key, query): @@ -444,18 +513,18 @@ def does_not_match_key_in_query(self, key, query_key, query): :rtype: Query """ dumped = query.dump() - dumped['className'] = query._query_class._class_name - self._add_condition(key, '$dontSelect', {'key': query_key, 'query': dumped}) + dumped["className"] = query._query_class._class_name + self._add_condition(key, "$dontSelect", {"key": query_key, "query": dumped}) return self def _or_query(self, queries): - dumped = [q.dump()['where'] for q in queries] - self._where['$or'] = dumped + dumped = [q.dump()["where"] for q in queries] + self._where["$or"] = dumped return self def _and_query(self, queries): - dumped = [q.dump()['where'] for q in queries] - self._where['$and'] = dumped + dumped = [q.dump()["where"] for q in queries] + self._where["$and"] = dumped def _quote(self, s): # return "\\Q" + s.replace("\\E", "\\E\\\\E\\Q") + "\\E" @@ -469,7 +538,7 @@ def contains(self, key, value): :param value: 需要包含的字符串 :rtype: Query """ - self._add_condition(key, '$regex', self._quote(value)) + self._add_condition(key, "$regex", self._quote(value)) return self def startswith(self, key, value): @@ -480,7 +549,8 @@ def startswith(self, key, value): :param value: 需要查询的字符串 :rtype: Query """ - self._add_condition(key, '$regex', '^' + self._quote(value)) + value = value if isinstance(value, six.text_type) else value.decode("utf-8") + self._add_condition(key, "$regex", "^" + self._quote(value)) return self def endswith(self, key, value): @@ -491,7 +561,8 @@ def endswith(self, key, value): :param value: 需要查询的字符串 :rtype: Query """ - self._add_condition(key, '$regex', self._quote(value) + '$') + value = value if isinstance(value, six.text_type) else value.decode("utf-8") + self._add_condition(key, "$regex", self._quote(value) + "$") return self def ascending(self, key): @@ -521,7 +592,7 @@ def descending(self, key): :param key: 排序字段名 :rtype: Query """ - self._order = ['-{0}'.format(key)] + self._order = ["-{0}".format(key)] return self def add_descending(self, key): @@ -531,7 +602,7 @@ def add_descending(self, key): :param key: 排序字段名 :rtype: Query """ - self._order.append('-{0}'.format(key)) + self._order.append("-{0}".format(key)) return self def near(self, key, point): @@ -543,9 +614,9 @@ def near(self, key, point): :rtype: Query """ if point is None: - raise ValueError('near query does not accept None') + raise ValueError("near query does not accept None") - self._add_condition(key, '$nearSphere', point) + self._add_condition(key, "$nearSphere", point) return self def within_radians(self, key, point, max_distance, min_distance=None): @@ -559,9 +630,9 @@ def within_radians(self, key, point, max_distance, min_distance=None): :rtype: Query """ self.near(key, point) - self._add_condition(key, '$maxDistance', max_distance) + self._add_condition(key, "$maxDistance", max_distance) if min_distance is not None: - self._add_condition(key, '$minDistance', min_distance) + self._add_condition(key, "$minDistance", min_distance) return self def within_miles(self, key, point, max_distance, min_distance=None): @@ -601,7 +672,7 @@ def within_geo_box(self, key, southwest, northeast): :param northeast: 限制范围东北角坐标 :rtype: Query """ - self._add_condition(key, '$within', {'$box': [southwest, northeast]}) + self._add_condition(key, "$within", {"$box": [southwest, northeast]}) return self def include(self, *keys): @@ -632,19 +703,19 @@ def select(self, *keys): class FriendshipQuery(Query): def __init__(self, query_class): super(FriendshipQuery, self).__init__(query_class) - if query_class in ('_Follower', 'Follower'): - self._friendship_tag = 'follower' - elif query_class in ('_Followee', 'Followee'): - self._friendship_tag = 'followee' + if query_class in ("_Follower", "Follower"): + self._friendship_tag = "follower" + elif query_class in ("_Followee", "Followee"): + self._friendship_tag = "followee" else: - raise TypeError('FriendshipQuery takes only follower or followee') + raise TypeError("FriendshipQuery takes only follower or followee") def _new_object(self): return leancloud.User() def _process_result(self, obj): content = obj[self._friendship_tag] - if content['__type'] == 'Pointer' and content['className'] == '_User': - del content['__type'] - del content['className'] + if content["__type"] == "Pointer" and content["className"] == "_User": + del content["__type"] + del content["className"] return content diff --git a/leancloud/relation.py b/leancloud/relation.py index df3981f7..7116d819 100644 --- a/leancloud/relation.py +++ b/leancloud/relation.py @@ -3,11 +3,12 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import leancloud from leancloud import operation -__author__ = 'asaka ' +__author__ = "asaka " class Relation(object): @@ -37,9 +38,9 @@ def _ensure_parent_and_key(self, parent=None, key=None): self.key = key if self.parent != parent: - raise TypeError('relation retrieved from two different object') + raise TypeError("relation retrieved from two different object") if self.key != key: - raise TypeError('relation retrieved from two different object') + raise TypeError("relation retrieved from two different object") def add(self, *obj_or_objs): """ @@ -49,7 +50,7 @@ def add(self, *obj_or_objs): """ objs = obj_or_objs if not isinstance(obj_or_objs, (list, tuple)): - objs = (obj_or_objs, ) + objs = (obj_or_objs,) change = operation.Relation(objs, ()) self.parent.set(self.key, change) self.target_class_name = change._target_class_name @@ -63,16 +64,13 @@ def remove(self, *obj_or_objs): """ objs = obj_or_objs if not isinstance(obj_or_objs, (list, tuple)): - objs = (obj_or_objs, ) + objs = (obj_or_objs,) change = operation.Relation((), objs) self.parent.set(self.key, change) self.target_class_name = change._target_class_name def dump(self): - return { - '__type': 'Relation', - 'className': self.target_class_name - } + return {"__type": "Relation", "className": self.target_class_name} @property def query(self): @@ -85,12 +83,12 @@ def query(self): if self.target_class_name is None: target_class = leancloud.Object.extend(self.parent._class_name) query = leancloud.Query(target_class) - query._extra['redirectClassNameForKey'] = self.key + query._extra["redirectClassNameForKey"] = self.key else: target_class = leancloud.Object.extend(self.target_class_name) query = leancloud.Query(target_class) - query._add_condition('$relatedTo', 'object', self.parent._to_pointer()) - query._add_condition('$relatedTo', 'key', self.key) + query._add_condition("$relatedTo", "object", self.parent._to_pointer()) + query._add_condition("$relatedTo", "key", self.key) return query diff --git a/leancloud/role.py b/leancloud/role.py index d92558c9..dec5c924 100644 --- a/leancloud/role.py +++ b/leancloud/role.py @@ -3,13 +3,15 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import re +import six + import leancloud -from leancloud._compat import string_types -__author__ = 'asaka ' +__author__ = "asaka " class Role(leancloud.Object): @@ -22,34 +24,54 @@ def __init__(self, name=None, acl=None): acl.set_public_read_access(True) self.set_acl(acl) + @property + def name(self): + return self.get("name") + + @name.setter + def name(self, name): + return self.set("name", name) + def get_name(self): """ 获取 Role 的 name,等同于 role.get('name') """ - return self.get('name') + return self.get("name") def set_name(self, name): """ 为 Role 设置 name,等同于 role.set('name', name) """ - return self.set('name', name) + return self.set("name", name) + + @property + def users(self): + return self.relation("users") def get_users(self): """ 获取当前 Role 下所有绑定的用户。 """ - return self.relation('users') + return self.relation("users") + + @property + def roles(self): + return self.relation("roles") def get_roles(self): - return self.relation('roles') + return self.relation("roles") def validate(self, attrs): - if 'name' in attrs and attrs['name'] != self.get_name(): - new_name = attrs['name'] - if not isinstance(new_name, string_types): - raise TypeError('role name must be string_types') - r = re.compile('^[0-9a-zA-Z\-_]+$') + if "name" in attrs and attrs["name"] != self.get_name(): + new_name = attrs["name"] + if not isinstance(new_name, six.string_types): + raise TypeError("role name must be string_types") + r = re.compile(r"^[0-9a-zA-Z\-_]+$") if not r.match(new_name): - raise TypeError('role\'s name can only contain alphanumeric characters, _, -, and spaces.') + raise TypeError( + """ + role's name can only contain alphanumeric characters, _, -, and spaces. + """ + ) return super(Role, self).validate(attrs) diff --git a/leancloud/status.py b/leancloud/status.py new file mode 100644 index 00000000..f708eb4a --- /dev/null +++ b/leancloud/status.py @@ -0,0 +1,132 @@ +# coding: utf-8 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + + +from collections import namedtuple +from datetime import datetime + +from leancloud import client +from leancloud import utils +from leancloud.query import Query +from leancloud.user import User + +__author__ = "asaka" + + +StatusesCount = namedtuple("StatusesCount", ["total", "unread"]) + + +class Status(object): + def __init__(self, **data): + self.id = None # type: str + self.created_at = None # type: datetime + self.updated_at = None # type: datetime + self._data = data + self.inbox_type = "default" + + def get(self, key): + return self._data.get(key) + + def set(self, key, value): + self._data[key] = value + return self + + def send(self, query): + current_user = User.get_current() + if not current_user: + raise ValueError("Please sign in an user") + + params = { + "inboxType": self.inbox_type, + "data": self._data, + "query": query.dump(), + "source": self._data.get("source") or current_user._to_pointer(), + } + params["query"]["className"] = query._query_class._class_name + + content = client.post("/statuses", params=params).json() + self.id = content["objectId"] + self.created_at = utils.decode("createdAt", content["createdAt"]) + + def send_to_followers(self): + current_user = User.get_current() + if not current_user: + raise ValueError("Please sign in an user") + + query = Query("_Follower").select("follower").equal_to("user", current_user) + self.send(query) + + def send_private_status(self, target): + current_user = User.get_current() + if not current_user: + raise ValueError("Please sign in an user") + + if not isinstance(target, User): + raise TypeError("target must be a leancloud.User") + + query = Query("User").equal_to("objectId", target.id) + self.inbox_type = "private" + self.send(query) + + def destroy(self): + if not self.id: + raise ValueError("This status is not saved") + client.delete("/statuses/" + self.id) + + def _update_data(self, server_data): + self.id = server_data.pop("objectId") + self.created_at = utils.decode("createdAt", server_data.pop("createdAt")) + self.updated_at = utils.decode("updatedAt", server_data.pop("updatedAt")) + self._data = utils.decode(None, server_data) + + @staticmethod + def count_unread_statuses(owner, inbox_type="default"): + params = { + "inboxType": inbox_type, + "owner": utils.encode(owner), + } + content = client.get("/subscribe/statuses/count", params).json() + return StatusesCount(total=content["total"], unread=content["unread"]) + + +class InboxQuery(Query): + def __init__(self): + super(InboxQuery, self).__init__("_Status") + self._since_id = 0 + self._max_id = 0 + self._inbox_type = "default" + self._owner = None + + def since_id(self, value): + self._since_id = value + return self + + def max_id(self, value): + self._max_id = value + return self + + def owner(self, value): + self._owner = value + return self + + def inbox_type(self, value): + self._inbox_type = value + return self + + def _new_object(self): + return Status() + + def _do_request(self, params): + return client.get("/subscribe/statuses", params).json() + + def dump(self): + result = super(InboxQuery, self).dump() + result["owner"] = utils.encode(self._owner) + result["inboxType"] = self._inbox_type + result["sinceId"] = self._since_id + result["maxId"] = self._max_id + return result diff --git a/leancloud/sys_message.py b/leancloud/sys_message.py new file mode 100644 index 00000000..c01bb012 --- /dev/null +++ b/leancloud/sys_message.py @@ -0,0 +1,87 @@ +# coding: utf-8 + +""" +实时通讯会话相关操作。 +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import arrow +from leancloud.object_ import Object + + +class SysMessage(Object): + @property + def conversation(self): + """ + 此消息对应的对话,对应 conv 字段。 + + :rtype: leancloud.Conversation + """ + return self.get("conv") + + @property + def message_id(self): + """ + 此消息 id,对应 msgId 字段。 + + :rtype: str + """ + return self.get("msgId") + + @property + def is_binary(self): + """ + 此消息内容是否为二进制,对应 bin 字段。 + + :rtype: bool + """ + return self.get("bin") + + @property + def from_client(self): + """ + 此消息发送者 cliend id,对应 client 字段。 + + :rtype: str + """ + return self.get("from") + + @property + def from_ip(self): + """ + 此消息发送者 IP,对应 fromIp 字段。 + + :rtype: str + """ + return self.get("fromIp") + + @property + def data(self): + """ + 此消息原始内容,如果需要的话,需要手动进行序列化,对应 data 字段。 + + :rtype: str + """ + return self.get("data") + + @property + def message_created_at(self): + """ + 此消息发送时间,对应 timestamp 字段。 + + :rtype: datetime + """ + return arrow.get(self.get("timestamp") / 1000).to("local").datetime + + @property + def ack_at(self): + """ + 此消息送达时间,对应 ackAt 字段。 + + :rtype: datetime + """ + return arrow.get(self.get("ackAt") / 1000).to("local").datetime diff --git a/leancloud/user.py b/leancloud/user.py index dd648457..22c4b576 100644 --- a/leancloud/user.py +++ b/leancloud/user.py @@ -3,16 +3,21 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import threading +from typing import Optional + +import six -from leancloud import FriendshipQuery from leancloud import client -from leancloud import Object -from leancloud._compat import string_types +from leancloud.errors import LeanCloudError +from leancloud.query import FriendshipQuery +from leancloud.object_ import Object +from leancloud.relation import Relation -__author__ = 'asaka' +__author__ = "asaka" thread_locals = threading.local() @@ -27,53 +32,67 @@ def __init__(self, **attrs): def get_session_token(self): return self._session_token + @property + def session_token(self): + return self._session_token + def _merge_metadata(self, attrs): - if 'sessionToken' in attrs: - self._session_token = attrs.pop('sessionToken') + if "sessionToken" in attrs: + self._session_token = attrs.pop("sessionToken") return super(User, self)._merge_metadata(attrs) @classmethod def create_follower_query(cls, user_id): - if not user_id or not isinstance(user_id, string_types): - raise TypeError('invalid user_id: {0}'.format(user_id)) - query = FriendshipQuery('_Follower') - query.equal_to('user', User.create_without_data(user_id)) + if not user_id or not isinstance(user_id, six.string_types): + raise TypeError("invalid user_id: {0}".format(user_id)) + query = FriendshipQuery("_Follower") + query.equal_to("user", User.create_without_data(user_id)) return query @classmethod def create_followee_query(cls, user_id): - if not user_id or not isinstance(user_id, string_types): - raise TypeError('invalid user_id: {0}'.format(user_id)) - query = FriendshipQuery('_Followee') - query.equal_to('user', User.create_without_data(user_id)) + if not user_id or not isinstance(user_id, six.string_types): + raise TypeError("invalid user_id: {0}".format(user_id)) + query = FriendshipQuery("_Followee") + query.equal_to("user", User.create_without_data(user_id)) return query @classmethod - def get_current(cls): - return getattr(thread_locals, 'current_user', None) + def get_current(cls): # type: () -> Optional[User] + return getattr(thread_locals, "current_user", None) + + @classmethod + def set_current(cls, user): + thread_locals.current_user = user @classmethod def become(cls, session_token): - response = client.get('/users/me', params={'session_token': session_token}) + """ + 通过 session token 获取用户对象 + + :param session_token: 用户的 session token + :return: leancloud.User + """ + response = client.get("/users/me", params={"session_token": session_token}) content = response.json() user = cls() user._update_data(content) user._handle_save_result(True) - if 'smsCode' not in content: - user._attributes.pop('smsCode', None) + if "smsCode" not in content: + user._attributes.pop("smsCode", None) return user @property def is_current(self): - if not getattr(thread_locals, 'current_user', None): + if not getattr(thread_locals, "current_user", None): return False return self.id == thread_locals.current_user.id def _cleanup_auth_data(self): if not self.is_current: return - auth_data = self.get('authData') + auth_data = self.get("authData") if not auth_data: return keys = list(auth_data.keys()) @@ -81,23 +100,12 @@ def _cleanup_auth_data(self): if not auth_data[key]: del auth_data[key] -# def _sync_all_auth_data(self): -# auth_data = self.get('authData') -# if not auth_data: -# return -# for key in auth_data.keys(): -# self._sync_auth_data(key) - -# def _sync_auth_data(self, key): -# if not self.is_current: -# return - def _handle_save_result(self, make_current=False): if make_current: - thread_locals.current_user = self + User.set_current(self) self._cleanup_auth_data() # self._sync_all_auth_data() - self._attributes.pop('password', None) + self._attributes.pop("password", None) def save(self, make_current=False): super(User, self).save() @@ -109,47 +117,53 @@ def sign_up(self, username=None, password=None): 用户对象上必须包含 username 和 password 两个字段 """ if username: - self.set('username', username) + self.set("username", username) if password: - self.set('password', password) + self.set("password", password) - username = self.get('username') + username = self.get("username") if not username: - raise TypeError('invalid username: {0}'.format(username)) - password = self.get('password') + raise TypeError("invalid username: {0}".format(username)) + password = self.get("password") if not password: - raise TypeError('invalid password') + raise TypeError("invalid password") self.save(make_current=True) - def login(self, username=None, password=None): + def login(self, username=None, password=None, email=None): """ - 登陆用户。如果用户名和密码正确,服务器会返回用户的 sessionToken 。 + 登录用户。成功登录后,服务器会返回用户的 sessionToken 。 + + :param username: 用户名 + :param email: 邮箱地址(username 和 email 这两个参数必须传入一个且仅能传入一个) + :param password: 用户密码 """ if username: - self.set('username', username) + self.set("username", username) if password: - self.set('password', password) - response = client.post('/login', params=self.dump()) + self.set("password", password) + if email: + self.set("email", email) + # 同时传入 username、email、password 的情况下,这三个字段会一起发给后端。 + # 这时后端会忽略 email,等价于只传 username 和 password。 + # 这里的 login 函数的实现依赖后端的这一行为,没有校验 username 和 email 中调用者传入且仅传入了其中一个参数。 + response = client.post("/login", params=self.dump()) content = response.json() self._update_data(content) self._handle_save_result(True) - if 'smsCode' not in content: - self._attributes.pop('smsCode', None) + if "smsCode" not in content: + self._attributes.pop("smsCode", None) def logout(self): if not self.is_current: return self._cleanup_auth_data() - thread_locals.current_user = None + del thread_locals.current_user @classmethod def login_with_mobile_phone(cls, phone_number, password): user = User() - params = { - 'mobilePhoneNumber': phone_number, - 'password': password - } + params = {"mobilePhoneNumber": phone_number, "password": password} user._update_data(params) user.login() return user @@ -161,8 +175,10 @@ def follow(self, target_id): :param target_id: 需要关注的用户的 id """ if self.id is None: - raise ValueError('Please sign in') - response = client.post('/users/{0}/friendship/{1}'.format(self.id, target_id), None) + raise ValueError("Please sign in") + response = client.post( + "/users/{0}/friendship/{1}".format(self.id, target_id), None + ) assert response.ok def unfollow(self, target_id): @@ -173,132 +189,183 @@ def unfollow(self, target_id): :return: """ if self.id is None: - raise ValueError('Please sign in') - response = client.delete('/users/{0}/friendship/{1}'.format(self.id, target_id), None) + raise ValueError("Please sign in") + response = client.delete( + "/users/{0}/friendship/{1}".format(self.id, target_id), None + ) assert response.ok @classmethod def login_with(cls, platform, third_party_auth_data): - ''' + """ 把第三方平台号绑定到 User 上 :param platform: 第三方平台名称 base string - ''' + """ user = User() return user.link_with(platform, third_party_auth_data) def link_with(self, provider, third_party_auth_data): if type(provider) != str: - raise TypeError('input should be a string') - auth_data = self.get('authData') + raise TypeError("input should be a string") + auth_data = self.get("authData") if not auth_data: auth_data = {} auth_data[provider] = third_party_auth_data - self.set('authData', auth_data) + self.set("authData", auth_data) self.save() self._handle_save_result(True) return self def unlink_from(self, provider): - ''' + """ 解绑特定第三方平台 - ''' + """ if type(provider) != str: - raise TypeError('input should be a string') + raise TypeError("input should be a string") self.link_with(provider, None) # self._sync_auth_data(provider) return self def is_linked(self, provider): try: - self.get('authData')[provider] + self.get("authData")[provider] except KeyError: return False return True @classmethod def signup_or_login_with_mobile_phone(cls, phone_number, sms_code): - ''' + """ param phone_nubmer: string_types param sms_code: string_types 在调用此方法前请先使用 request_sms_code 请求 sms code - ''' - data = { - 'mobilePhoneNumber': phone_number, - 'smsCode': sms_code - } - response = client.post('/usersByMobilePhone', data) + """ + data = {"mobilePhoneNumber": phone_number, "smsCode": sms_code} + response = client.post("/usersByMobilePhone", data) content = response.json() user = cls() user._update_data(content) user._handle_save_result(True) - if 'smsCode' not in content: - user._attributes.pop('smsCode', None) + if "smsCode" not in content: + user._attributes.pop("smsCode", None) return user def update_password(self, old_password, new_password): - route = '/users/' + self.id + '/updatePassword' - params = { - 'old_password': old_password, - 'new_password': new_password - } + route = "/users/" + self.id + "/updatePassword" + params = {"old_password": old_password, "new_password": new_password} content = client.put(route, params).json() self._update_data(content) self._handle_save_result(True) def get_username(self): - return self.get('username') + return self.get("username") def get_mobile_phone_number(self): - return self.get('mobilePhoneNumber') + return self.get("mobilePhoneNumber") def set_mobile_phone_number(self, phone_number): - return self.set('mobilePhoneNumber', phone_number) + return self.set("mobilePhoneNumber", phone_number) def set_username(self, username): - return self.set('username', username) + return self.set("username", username) def set_password(self, password): - return self.set('password', password) + return self.set("password", password) def set_email(self, email): - return self.set('email', email) + return self.set("email", email) def get_email(self): - return self.get('email') + return self.get("email") + + def get_roles(self): + return Relation.reverse_query("_Role", "users", self).find() + + def refresh_session_token(self): + """ + 重置当前用户 `session token`。 + 会使其他客户端已登录用户登录失效。 + """ + response = client.put("/users/{}/refreshSessionToken".format(self.id), None) + content = response.json() + self._update_data(content) + self._handle_save_result(False) + + def is_authenticated(self): + """ + 判断当前用户对象是否已登录。 + 会先检查此用户对象上是否有 `session_token`,如果有的话,会继续请求服务器验证 `session_token` 是否合法。 + """ + session_token = self.get_session_token() + if not session_token: + return False + try: + response = client.get("/users/me", params={"session_token": session_token}) + except LeanCloudError as e: + if e.code == 211: + return False + else: + raise + return response.status_code == 200 + + @classmethod + def request_password_reset(cls, email): + params = {"email": email} + client.post("/requestPasswordReset", params) @classmethod - def request_password_reset(self, email): - params = {'email': email} - client.post('/requestPasswordReset', params) + def request_email_verify(cls, email): + params = {"email": email} + client.post("/requestEmailVerify", params) @classmethod - def request_email_verify(self, email): - params = {'email': email} - client.post('/requestEmailVerify', params) + def request_mobile_phone_verify(cls, phone_number, validate_token=None): + params = {"mobilePhoneNumber": phone_number} + if validate_token is not None: + params["validate_token"] = validate_token + client.post("/requestMobilePhoneVerify", params) @classmethod - def request_mobile_phone_verify(cls, phone_number): - params = {'mobilePhoneNumber': phone_number} - client.post('/requestMobilePhoneVerify', params) + def request_password_reset_by_sms_code(cls, phone_number, validate_token=None): + params = {"mobilePhoneNumber": phone_number} + if validate_token is not None: + params["validate_token"] = validate_token + client.post("/requestPasswordResetBySmsCode", params) @classmethod - def request_password_reset_by_sms_code(cls, phone_number): - params = {'mobilePhoneNumber': phone_number} - client.post('/requestPasswordResetBySmsCode', params) + def reset_password_by_sms_code(cls, sms_code, new_password, phone_number): + params = {"password": new_password, "mobilePhoneNumber": phone_number} + client.put("/resetPasswordBySmsCode/" + sms_code, params) + # This should be an instance method. + # However, to be consistent with other similar methods (`request_password_reset_by_sms_code`), + # it is implemented as a class method. + @classmethod + def request_change_phone_number(cls, phone_number, ttl=None, validate_token=None): + params = {"mobilePhoneNumber": phone_number} + if ttl is not None: + params["ttl"] = ttl + if validate_token is not None: + params["validate_token"] = validate_token + client.post("/requestChangePhoneNumber", params) + + # This should be an instance method and update the local date, + # but it is implemented as a class method for the same reason as above. @classmethod - def reset_password_by_sms_code(cls, phone_number, new_password): - params = {'password' : new_password} - client.post("resetPasswordBySmsCode", params) - + def change_phone_number(cls, sms_code, phone_number): + params = {"mobilePhoneNumber": phone_number, "code": sms_code} + client.post("/changePhoneNumber", params) @classmethod - def verify_mobile_phone_number(cls, sms_code): - client.post('/verfyMobilePhone/' + sms_code, {}) + def verify_mobile_phone_number(cls, sms_code, phone_number): + params = {"mobilePhoneNumber": phone_number} + client.post("/verifyMobilePhone/" + sms_code, params) @classmethod - def request_login_sms_code(cls, phone_number): - params = {'mobilePhoneNumber': phone_number} - client.post('/requestLoginSmsCode', params) + def request_login_sms_code(cls, phone_number, validate_token=None): + params = {"mobilePhoneNumber": phone_number} + if validate_token is not None: + params["validate_token"] = validate_token + client.post("/requestLoginSmsCode", params) diff --git a/leancloud/utils.py b/leancloud/utils.py index be590364..1474cb62 100644 --- a/leancloud/utils.py +++ b/leancloud/utils.py @@ -3,26 +3,23 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import copy -import json -import gzip -import hashlib -import hmac +import functools from datetime import datetime +from datetime import timedelta import arrow +import six import iso8601 -from werkzeug import LocalProxy -from dateutil import tz +from werkzeug.local import LocalProxy +import dateutil.tz as tz import leancloud from leancloud import operation -from leancloud._compat import BytesIO -from leancloud._compat import iteritems -from leancloud._compat import to_bytes -__author__ = 'asaka ' +__author__ = "asaka " def get_dumpable_types(): @@ -35,7 +32,7 @@ def get_dumpable_types(): ) -def encode(value, disallow_objects=False): +def encode(value, disallow_objects=False, dump_objects=False): if isinstance(value, LocalProxy): value = value._get_current_object() @@ -44,33 +41,41 @@ def encode(value, disallow_objects=False): if tzinfo is None: tzinfo = tz.tzlocal() return { - '__type': 'Date', - 'iso': arrow.get(value, tzinfo).to('utc').format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z', + "__type": "Date", + "iso": arrow.get(value, tzinfo).to("utc").format("YYYY-MM-DDTHH:mm:ss.SSS") + + "Z", } if isinstance(value, leancloud.Object): if disallow_objects: - raise ValueError('leancloud.Object not allowed') + raise ValueError("leancloud.Object not allowed") + if dump_objects: + return value._dump() return value._to_pointer() if isinstance(value, leancloud.File): if not value.url and not value.id: - raise ValueError('Tried to save an object containing an unsaved file.') + raise ValueError("Tried to save an object containing an unsaved file.") return { - '__type': 'File', - 'id': value.id, - 'name': value.name, - 'url': value.url, + "__type": "File", + "id": value.id, + "name": value.name, + "url": value.url, } if isinstance(value, get_dumpable_types()): return value.dump() if isinstance(value, (tuple, list)): - return [encode(x, disallow_objects) for x in value] + return [encode(x, disallow_objects, dump_objects) for x in value] if isinstance(value, dict): - return dict([(k, encode(v, disallow_objects)) for k, v in iteritems(value)]) + return dict( + [ + (k, encode(v, disallow_objects, dump_objects)) + for k, v in six.iteritems(value) + ] + ) return value @@ -85,58 +90,92 @@ def decode(key, value): if not isinstance(value, dict): return value - if '__type' not in value: - return dict([(k, decode(k, v)) for k, v in iteritems(value)]) + if key == "ACL": + if isinstance(value, leancloud.ACL): + return value + return leancloud.ACL(value) - _type = value['__type'] + if "__type" not in value: + return dict([(k, decode(k, v)) for k, v in six.iteritems(value)]) - if _type == 'Pointer': + _type = value["__type"] + + if _type == "Pointer": value = copy.deepcopy(value) - class_name = value['className'] + class_name = value["className"] pointer = leancloud.Object.create(class_name) - if 'createdAt' in value: - value.pop('__type') - value.pop('className') + if "createdAt" in value: + value.pop("__type") + value.pop("className") pointer._update_data(value) else: - pointer._update_data({'objectId': value['objectId']}) + pointer._update_data({"objectId": value["objectId"]}) return pointer - if _type == 'Object': + if _type == "Object": value = copy.deepcopy(value) - class_name = value['className'] - value.pop('__type') - value.pop('className') + class_name = value["className"] + value.pop("__type") + value.pop("className") obj = leancloud.Object.create(class_name) obj._update_data(value) return obj - if _type == 'Date': - return arrow.get(iso8601.parse_date(value['iso'])).to('local').datetime + if _type == "Date": + return arrow.get(iso8601.parse_date(value["iso"])).to("local").datetime - if _type == 'GeoPoint': - return leancloud.GeoPoint(latitude=value['latitude'], longitude=value['longitude']) + if _type == "GeoPoint": + return leancloud.GeoPoint( + latitude=value["latitude"], longitude=value["longitude"] + ) - if key == 'ACL': - if isinstance(value, leancloud.ACL): - return value - return leancloud.ACL(value) - - if _type == 'Relation': + if _type == "Relation": relation = leancloud.Relation(None, key) - relation.target_class_name = value['className'] + relation.target_class_name = value["className"] return relation - if _type == 'File': - f = leancloud.File(value['name']) - meta_data = value.get('metaData') - if meta_data: + if _type == "File": + f = leancloud.File(value["name"]) + meta_data = value.get("metaData") + file_key = value.get("key") + if file_key is not None: + f.key = file_key + if meta_data is not None: f._metadata = meta_data - f._url = value['url'] - f.id = value['objectId'] + f._url = value["url"] + f._successful_url = value["url"] + f.id = value["objectId"] + f.created_at = decode_date_string(value.get("createdAt")) + f.updated_at = decode_date_string(value.get("updatedAt")) return f +def decode_date_string(date_or_string): + if date_or_string is None: + return None + elif isinstance(date_or_string, six.string_types): + return decode_date_string({"__type": "Date", "iso": date_or_string}) + elif date_or_string["__type"] == "Date": + return arrow.get(iso8601.parse_date(date_or_string["iso"])).to("local").datetime + else: + raise TypeError("Invalid date type") + + +def decode_updated_at(updated_at_date_string, created_at_datetime): + updated_at = decode_date_string(updated_at_date_string) + if updated_at is None: + if created_at_datetime is None: + return None + else: + # When a new object is created, updatedAt will be set as the same value as createdAt on the cloud. + # However, the cloud will only return objectId and createdAt in REST API response, without updatedAt. + # Thus we need to set updatedAt as the same value as createdAt, consistent with the value on the cloud. + # This behaviour is consistent with other SDKs such as JavaScript and Go. + return created_at_datetime + else: + return updated_at + + def traverse_object(obj, callback, seen=None): seen = seen or set() @@ -158,7 +197,7 @@ def traverse_object(obj, callback, seen=None): return callback(obj) if isinstance(obj, dict): - for key, child in iteritems(obj): + for key, child in six.iteritems(obj): new_child = traverse_object(child, callback, seen) if new_child: obj[key] = new_child @@ -167,8 +206,26 @@ def traverse_object(obj, callback, seen=None): return callback(obj) -def sign_disable_hook(hook_name, master_key, timestamp): - sign = hmac.new(to_bytes(master_key), - to_bytes('{0}:{1}'.format(hook_name, timestamp)), - hashlib.sha1).hexdigest() - return '{0},{1}'.format(timestamp, sign) +class throttle(object): + def __init__(self, seconds=0, minutes=0, hours=0): + self.throttle_period = timedelta(seconds=seconds, minutes=minutes, hours=hours) + self.time_of_last_call = datetime.min + + def __call__(self, fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + now = datetime.now() + time_since_last_call = now - self.time_of_last_call + if time_since_last_call > self.throttle_period: + self.time_of_last_call = now + return fn(*args, **kwargs) + + return wrapper + + +class classproperty(object): + def __init__(self, f): + self.f = f + + def __get__(self, obj, owner): + return self.f(owner) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..4f125651 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +arrow>=0.17.0,<1.0.0; python_version < '3.6' +arrow>=1.0.0,<2.0.0; python_version >= '3.6' +iso8601>=0.1.14 +six>=1.11.0 +qiniu>=7.3.1 +requests<=2.31.0; python_version >= '3.7' +urllib3<=1.26.18; python_version >= '3.7' +requests-toolbelt==1.0.0 +Werkzeug>=0.16.0,<2.0.0 +secure-cookie>=0.1.0,<1.0.0 +gevent>=22.10.2,<23.0.0 +typing; python_version < '3.5' +markupsafe<=2.0.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..7c2b2874 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 \ No newline at end of file diff --git a/setup.py b/setup.py index 96193cd6..33a7a6c4 100644 --- a/setup.py +++ b/setup.py @@ -5,50 +5,45 @@ here = path.abspath(path.dirname(__file__)) -extra_require = { - 'dev': ['sphinx'], - 'test': ['nose', 'coverage', 'wsgi_intercept'], - } - -if sys.version_info.major == 2: - extra_require['test'].append('typing') +install_requires = [ + "arrow>=0.17.0,<1.0.0; python_version < '3.6'", + "arrow>=1.0.0,<2.0.0; python_version >= '3.6'", + 'iso8601>=0.1.14', + 'six>=1.11.0', + 'qiniu==7.3.1', + "requests<=2.31.0; python_version >= '3.7'", + "urllib3<=1.26.18; python_version >= '3.7'", + 'requests-toolbelt>=1.0.0', + 'Werkzeug>=0.16.0,<2.0.0', + 'secure-cookie>=0.1.0,<1.0.0', + 'gevent>=22.10.2,<23.0.0', + "typing; python_version < '3.5'", + 'markupsafe<=2.0.1', +] setup( - name='leancloud-sdk', - version='1.6.1', + name='leancloud', + version='3.0.2', description='LeanCloud Python SDK', - url='https://leancloud.cn/', - author='asaka', author_email='lan@leancloud.rocks', - license='LGPL', - classifiers=[ - 'Development Status :: 4 - Beta', - + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.9', ], - keywords='Leancloud SDK', - packages=find_packages(exclude=['docs', 'tests*']), - test_suite='nose.collector', - - install_requires=[ - 'arrow', - 'iso8601', - 'qiniu', - 'requests', - 'werkzeug', - ], - - extras_require=extra_require + install_requires=install_requires, + extras_require={ + 'dev': ['sphinx', 'sphinx_rtd_theme'], + 'test': ['nose', 'wsgi_intercept', 'flask'], + } ) diff --git a/stubs/leancloud/__init__.pyi b/stubs/leancloud/__init__.pyi index e69de29b..48509e6b 100644 --- a/stubs/leancloud/__init__.pyi +++ b/stubs/leancloud/__init__.pyi @@ -0,0 +1,15 @@ +from . import client +from . import push +from .acl import ACL +from .file_ import File +from .geo_point import GeoPoint +from .object_ import Object +from .query import Query +from .relation import Relation +from .role import Role +from .user import User +from .client import init +from .client import use_master_key +from .client import use_production +from .client import use_region +from .push import Installation diff --git a/stubs/leancloud/acl.pyi b/stubs/leancloud/acl.pyi index 50aad342..4673a006 100644 --- a/stubs/leancloud/acl.pyi +++ b/stubs/leancloud/acl.pyi @@ -9,7 +9,7 @@ user_type = Union[str, User, Role] role_type = Union[Role, str] class ACL(object): - + permissions_by_id = ... #type: Dict def __init__(self, permissions_by_id: Dict=None) -> None: ... def dump(self) -> Dict:... diff --git a/stubs/leancloud/client.pyi b/stubs/leancloud/client.pyi new file mode 100644 index 00000000..00d953ab --- /dev/null +++ b/stubs/leancloud/client.pyi @@ -0,0 +1,15 @@ +import datetime + +USE_PRODUCTION = ... # type: str +REGION = ... # type: str +USE_MASTER_KEY = ... # type: bool + +def init(app_id: str, app_key: str=None, master_key: str=None) -> None: ... + +def use_region(region: str) -> None: ... + +def use_production(flag: bool) -> None: ... + +def use_master_key(flag: bool=True) -> None: ... + +def get_server_time() -> datetime.datetime: ... diff --git a/stubs/leancloud/file_.pyi b/stubs/leancloud/file_.pyi index 05886bf2..ba8a2421 100644 --- a/stubs/leancloud/file_.pyi +++ b/stubs/leancloud/file_.pyi @@ -2,7 +2,7 @@ import sys import six from six import StringIO, BytesIO -from typing import Union, Dict, SupportsFloat, Any +from typing import Union, Dict, SupportsFloat, Any, IO import leancloud from leancloud.object_ import Object @@ -10,8 +10,7 @@ from leancloud.acl import ACL if six.PY3: - from io import IOBase - file_type = IOBase + file_type = IO[Any] buffer_type = memoryview else: file_type = file @@ -19,8 +18,13 @@ else: class File(object): + id =... # type: str + _metadata = ... #type: Dict[str, Any] + _type = ... #type: str + def __init__(self, name: str, data: Union[StringIO, BytesIO, file_type, buffer_type]=None, type_: str=None) -> None:... + @classmethod def create_with_url(cls, name: str, url: str, meta_data: Dict =None, type_: str=None) -> File:... @@ -46,7 +50,7 @@ class File(object): @property def metadata(self) -> Dict:... - def get_thumbnail_url(self, width: SupportsFloat, height: SupportsFloat, quality: SupportsFloat, scale_to_fit: bool=True, fmt: str='png') -> str:... + def get_thumbnail_url(self, width: SupportsFloat, height: SupportsFloat, quality: SupportsFloat=100, scale_to_fit: bool=True, fmt: str='png') -> str:... def destroy(self) -> None:... diff --git a/stubs/leancloud/geo_point.pyi b/stubs/leancloud/geo_point.pyi index 92053496..f7cb6b63 100644 --- a/stubs/leancloud/geo_point.pyi +++ b/stubs/leancloud/geo_point.pyi @@ -2,7 +2,7 @@ from typing import SupportsFloat, Any class GeoPoint(object): - def __init__(self, latitude: SupportsFloat, longitude: SupportsFloat) -> None:... + def __init__(self, latitude: SupportsFloat=0, longitude: SupportsFloat=0) -> None:... @classmethod def _validate(cls, latitude: SupportsFloat, longitude, SupportsFloat) -> None:... diff --git a/stubs/leancloud/object_.pyi b/stubs/leancloud/object_.pyi index 1f7d9821..fa10c600 100644 --- a/stubs/leancloud/object_.pyi +++ b/stubs/leancloud/object_.pyi @@ -1,5 +1,4 @@ from typing import Any, Dict, Union, SupportsFloat - import leancloud from leancloud.query import Query from leancloud.acl import ACL @@ -7,6 +6,10 @@ from leancloud.relation import Relation class Object(object): + id = ... # type: str + _class_name = ... # type: str + _attributes = ... # type: Dict + def __init__(self, **attrs) -> None: ... # the real implementation of query property is on metaclass @@ -33,6 +36,8 @@ class Object(object): def dump(self) -> Object:... + def is_dirty(self, attr: str=None) -> bool:... + def destroy(self) -> None:... def save(self, where: Query=None) -> None:... @@ -51,6 +56,8 @@ class Object(object): def add(self, attr: str, item: Any) -> Object:... + def add_unique(self, attr: str, item: Any) -> Object:... + def remove(self, attr: str, item: Any) -> Object:... def clear(self) -> Object:... diff --git a/stubs/leancloud/push.pyi b/stubs/leancloud/push.pyi index 94f76fba..0a43074b 100644 --- a/stubs/leancloud/push.pyi +++ b/stubs/leancloud/push.pyi @@ -6,4 +6,6 @@ import leancloud from leancloud.query import Query -def send(data: Dict, channels: Union[List,Tuple]=None, push_time: datetime=None, expiration_time: datetime=None, expiration_interval: int=None, where: Query=None, cql: str=None) -> object:... +class Installation(leancloud.object_.Object): pass + +def send(data: Dict, channels: Union[List,Tuple]=None, push_time: datetime=None, expiration_time: datetime=None, expiration_interval: int=None, where: Query=None, cql: str=None) -> Installation:... diff --git a/stubs/leancloud/query.pyi b/stubs/leancloud/query.pyi index 38859a35..00a54be1 100644 --- a/stubs/leancloud/query.pyi +++ b/stubs/leancloud/query.pyi @@ -1,5 +1,4 @@ from typing import Union, Any, SupportsFloat, Tuple - import leancloud from leancloud.object_ import Object from leancloud.geo_point import GeoPoint @@ -9,7 +8,12 @@ class CQLResult(object): def __init__(self, results: List[Object], count: SupportsFloat, class_name: str) -> None:... class Query(object): - def __init__(self, query_class: Union[str, Object]) -> None:... + _skip = ... # type: int + _limit = ... # type: int + _friendship_tag = ... # type: str + + + def __init__(self, query_class: Union[str, Object, type]) -> None:... @classmethod def or_(cls, *queries: Query) -> Query:... @@ -52,11 +56,13 @@ class Query(object): def contains_all(self, key: str, values: Union[List[Any],Tuple[Any]]) -> Query:... - def exitst(self, key: str) -> Query:... + def exists(self, key: str) -> Query:... + + def does_not_exist(self, key: str) -> Query:... - def does_not_exists(self, key: str) -> Query:... + def does_not_exists(self, key:str) -> Query:... - def matched(self, key: str, regex: str, ignore_case: bool=False, muliti_line: bool=False) -> Query:... + def matched(self, key: str, regex: str, ignore_case: bool=False, multi_line: bool=False) -> Query:... def matches_query(self, key: str, value: Any) -> Query:... @@ -70,9 +76,9 @@ class Query(object): def contains(self, key: str, value: Any) -> Query:... - def startwith(self, key: str, value: Any) -> Query:... + def startswith(self, key: str, value: Any) -> Query:... - def endwith(self, key: str, value: Any) -> Query:... + def endswith(self, key: str, value: Any) -> Query:... def ascending(self, key: str) -> Query:... @@ -90,8 +96,8 @@ class Query(object): def within_kilometers(self, key: str, point: GeoPoint, max_distance: SupportsFloat, min_distance: SupportsFloat=None) -> Query:... - def within_geo_box(self, key: str, southwest: Union[List, Tuple], northeast: Union[List, Tuple]) -> Query:... + def within_geo_box(self, key: str, southwest:Tuple[SupportsFloat, SupportsFloat], northeast: Tuple[SupportsFloat, SupportsFloat]) -> Query:... - def include(self, *keys: str) -> Query:... + def include(self, *keys: Union[List[str], Tuple[str]]) -> Query:... - def select(self, *keys: str) -> Query:... + def select(self, *keys: Union[List[str], Tuple[str]]) -> Query:... diff --git a/stubs/leancloud/relation.pyi b/stubs/leancloud/relation.pyi index bbcfd3dd..4ce6ec92 100644 --- a/stubs/leancloud/relation.pyi +++ b/stubs/leancloud/relation.pyi @@ -11,6 +11,8 @@ class Relation(object): @classmethod def reverse_query(cls, parent_class: Union[str, Object], relation_key: str, child: Object) -> Query:... + def _ensure_parent_and_key(self, parent: Object=None, key: str=None) -> None:... + def add(self, *obj_or_objs: Union[Object, List[Object]]) -> None:... def remove(slef, *obj_or_objs: Union[object, List[Object]]) -> None:... diff --git a/stubs/leancloud/role.pyi b/stubs/leancloud/role.pyi index 74dfa7fa..c1033f44 100644 --- a/stubs/leancloud/role.pyi +++ b/stubs/leancloud/role.pyi @@ -5,7 +5,7 @@ from leancloud.acl import ACL from leancloud.relation import Relation -class Role(object): +class Role(leancloud.Object): def __init__(self, name: str=None, acl: ACL=None) -> None:... def get_name(self) -> str:... diff --git a/stubs/leancloud/user.pyi b/stubs/leancloud/user.pyi index f60cf8e0..65854645 100644 --- a/stubs/leancloud/user.pyi +++ b/stubs/leancloud/user.pyi @@ -1,11 +1,14 @@ from typing import Any, Dict - import leancloud +from leancloud.role import Role from leancloud.object_ import Object from leancloud.query import Query + + class User(Object): + _session_token = ... # type: str def get_session_token(self) -> str:... @@ -68,6 +71,8 @@ class User(Object): def get_email(self) -> User:... + def get_roles(self): -> List[Role] + @classmethod def request_password_reset(self, email: str) -> None:... diff --git a/tests/__init__.py b/tests/__init__.py index 9ec7b397..9efe8a72 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,14 +4,25 @@ from __future__ import division from __future__ import print_function -import requests import os +import warnings + +import requests + import leancloud +warnings.filterwarnings("ignore") requests.packages.urllib3.disable_warnings() -os.environ['APP_ID'] = os.environ.get('APP_ID', 'pgk9e8orv8l9coak1rjht1avt2f4o9kptb0au0by5vbk9upb') -os.environ['APP_KEY'] = os.environ.get('APP_KEY', 'hi4jsm62kok2qz2w2qphzryo564rzsrucl2czb0hn6ogwwnd') -os.environ['MASTER_KEY'] = os.environ.get('MASTER_KEY', 'azkuvukzlq3t38abdrgrwqqdcx9me6178ctulhd14wynfq1n') -leancloud.use_region(os.environ.get('USE_REGION', 'CN')) +os.environ["APP_ID"] = os.environ.get( + "APP_ID", "pgk9e8orv8l9coak1rjht1avt2f4o9kptb0au0by5vbk9upb" +) +os.environ["APP_KEY"] = os.environ.get( + "APP_KEY", "hi4jsm62kok2qz2w2qphzryo564rzsrucl2czb0hn6ogwwnd" +) +os.environ["MASTER_KEY"] = os.environ.get( + "MASTER_KEY", "azkuvukzlq3t38abdrgrwqqdcx9me6178ctulhd14wynfq1n" +) +os.environ["HOOK_KEY"] = os.environ.get("HOOK_KEY", "YWR0Gy0MQRf2s4vIaFTT9pPp") +leancloud.use_region(os.environ.get("USE_REGION", "CN")) diff --git a/tests/mypy_test.py b/tests/mypy_test.py new file mode 100644 index 00000000..a65c70fd --- /dev/null +++ b/tests/mypy_test.py @@ -0,0 +1,44 @@ +# coding: utf-8 +import ast +import os +import re +import tempfile + + +# this script should be excuted under python_SDK/tests +def main(): + file_list = [ + f + for f in os.listdir(".") + if re.search(r"^test_.*\.py$", f) and f != ("test_mypy.py" or "test_engine.py") + ] + temp_dir = tempfile.gettempdir() + + for f in file_list: + f_buf = open(f) + f_text = f_buf.read() + f_buf.close() + file_name = "mypy_" + f + + nodes = ast.walk(ast.parse(f_text)) + tests = [ + node.name + for node in nodes + if type(node) == ast.FunctionDef and node.name.startswith("test") + ] + + test_file = temp_dir + "/" + file_name + mypy_test = open(test_file, "w") + mypy_test.write("import {}\n\n".format(f[:-3])) + middle = "()\n" + f[:-3] + "." + funcs = middle.join(tests) + funcs += "()" + funcs = f[:-3] + "." + funcs + mypy_test.write(funcs) + mypy_test.close() + os.system("mypy {}".format(test_file)) + os.remove(test_file) + + +if __name__ == "__main__": + main() diff --git a/tests/request_generator.py b/tests/request_generator.py index 50ef82de..e325f927 100644 --- a/tests/request_generator.py +++ b/tests/request_generator.py @@ -7,13 +7,11 @@ import time import hashlib -from leancloud._compat import to_bytes - def generate_request(app_key, master_key=False): md5sum = hashlib.md5() current_time = str(int(time.time() * 1000)) - md5sum.update(to_bytes(current_time + app_key)) + md5sum.update((current_time + app_key).encode("utf-8")) md5sum = md5sum.hexdigest() if master_key: return md5sum + "," + current_time + ",master" diff --git a/tests/test_acl.py b/tests/test_acl.py index 40b84341..1058c95d 100644 --- a/tests/test_acl.py +++ b/tests/test_acl.py @@ -4,114 +4,114 @@ from __future__ import division from __future__ import print_function -from nose.tools import raises +from nose.tools import raises # type: ignore from leancloud import acl from leancloud import role from leancloud import user -def test_dump(): +def test_dump(): # type: () -> None user_acl = acl.ACL() assert user_acl.dump() == {} -def test_set_access(): +def test_set_access(): # type: () -> None user_acl = acl.ACL() - assert user_acl._set_access('read', 520, False) == None - user_acl.set_read_access(520, True) - user_acl._set_access('read', 520, False) - assert user_acl.permissions_by_id.get('read', 'Not Exist') == 'Not Exist' + user_acl._set_access("read", "520", False) + user_acl.set_read_access("520", True) + user_acl._set_access("read", "520", False) + assert user_acl.permissions_by_id.get("read", "Not Exist") == "Not Exist" -def test_get_access(): +def test_get_access(): # type: () -> None user_acl = acl.ACL() - assert user_acl._get_access('read', '520') == False + assert user_acl._get_access("read", "520") is False -def test_set_and_get_read_access(): +def test_set_and_get_read_access(): # type: () -> None user_acl = acl.ACL() - user_acl.set_read_access(520, True) - assert user_acl.permissions_by_id[520]['read'] - assert user_acl.get_read_access(520) + user_acl.set_read_access("520", True) + assert user_acl.permissions_by_id["520"]["read"] + assert user_acl.get_read_access("520") user_acl = acl.ACL() test_user = user.User() - test_user.id = 520 + test_user.id = "520" user_acl.set_read_access(test_user, True) assert user_acl.get_read_access(test_user) role_acl = acl.ACL() - test_role = role.Role('520', role_acl) + test_role = role.Role("520", role_acl) role_acl.set_read_access(test_role, True) assert role_acl.get_read_access(test_role) -def test_set_and_get_write_access(): +def test_set_and_get_write_access(): # type: () -> None user_acl = acl.ACL() - user_acl.set_write_access(520, True) - assert user_acl.permissions_by_id[520]['write'] - assert user_acl.get_write_access(520) + user_acl.set_write_access("520", True) + assert user_acl.permissions_by_id["520"]["write"] + assert user_acl.get_write_access("520") -def test_set_and_get_public_read_access(): +def test_set_and_get_public_read_access(): # type: () -> None user_acl = acl.ACL() user_acl.set_public_read_access(True) - assert user_acl.permissions_by_id['*']['read'] + assert user_acl.permissions_by_id["*"]["read"] assert user_acl.get_public_read_access() user_acl.set_public_read_access(False) - assert not user_acl.permissions_by_id.get('*') + assert not user_acl.permissions_by_id.get("*") assert not user_acl.get_public_read_access() -def test_set_and_get_public_write_access(): +def test_set_and_get_public_write_access(): # type: () -> None user_acl = acl.ACL() user_acl.set_public_write_access(True) - assert user_acl.permissions_by_id['*']['write'] + assert user_acl.permissions_by_id["*"]["write"] assert user_acl.get_public_write_access() - -def test_first_set_read_ture_and_then_write_false(): + +def test_first_set_read_ture_and_then_write_false(): # type: () -> None user_acl = acl.ACL() - user_acl.set_read_access(520, True) - user_acl.set_write_access(520, False) - + user_acl.set_read_access("520", True) + user_acl.set_write_access("520", False) + -def test_set_and_get_role_read_access(): +def test_set_and_get_role_read_access(): # type: () -> None role_acl = acl.ACL() - test_role = role.Role('520', role_acl) + test_role = role.Role("520", role_acl) role_acl.set_role_read_access(test_role, True) - assert role_acl.permissions_by_id['role:520']['read'] + assert role_acl.permissions_by_id["role:520"]["read"] assert role_acl.get_role_read_access(test_role) @raises(TypeError) -def test_set_role_read_access_error(): +def test_set_role_read_access_error(): # type: () -> None role_acl = acl.ACL() - role_acl.set_role_read_access(510, True) + role_acl.set_role_read_access(510, True) # type: ignore @raises(TypeError) -def test_get_role_read_access_error(): +def test_get_role_read_access_error(): # type: () -> None role_acl = acl.ACL() - role_acl.get_role_read_access(510) + role_acl.get_role_read_access(510) # type: ignore -def test_set_and_get_role_write_access(): +def test_set_and_get_role_write_access(): # type: () -> None role_acl = acl.ACL() - test_role = role.Role('520', role_acl) + test_role = role.Role("520", role_acl) role_acl.set_role_write_access(test_role, True) - assert role_acl.permissions_by_id['role:520']['write'] + assert role_acl.permissions_by_id["role:520"]["write"] assert role_acl.get_role_write_access(test_role) @raises(TypeError) -def test_set_get_role_write_access_error(): +def test_set_get_role_write_access_error(): # type: () -> None role_acl = acl.ACL() - role_acl.set_role_write_access(510, True) + role_acl.set_role_write_access(510, True) # type: ignore @raises(TypeError) -def test_get_role_write_access_error(): +def test_get_role_write_access_error(): # type: () -> None role_acl = acl.ACL() - role_acl.get_role_write_access(510) + role_acl.get_role_write_access(510) # type: ignore diff --git a/tests/test_client.py b/tests/test_client.py index cd89fda0..fbf5a644 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,91 +5,27 @@ from __future__ import print_function import os -import json -import datetime - -from werkzeug.wrappers import Request -from wsgi_intercept import requests_intercept, add_wsgi_intercept import leancloud from leancloud import client -from leancloud.app_router import AppRouter - -__author__ = 'asaka' +__author__ = "asaka" -def test_use_production(): - assert client.USE_PRODUCTION == '1' +def test_use_production(): # type: () -> None + assert client.USE_PRODUCTION == "1" leancloud.use_production(False) - assert client.USE_PRODUCTION == '0' + assert client.USE_PRODUCTION == "0" leancloud.use_production(True) - assert client.USE_PRODUCTION == '1' + assert client.USE_PRODUCTION == "1" -def test_use_master_key(): - leancloud.init(os.environ['APP_ID'], os.environ['APP_KEY'], os.environ['MASTER_KEY']) +def test_use_master_key(): # type: () -> None + leancloud.init( + os.environ["APP_ID"], os.environ["APP_KEY"], os.environ["MASTER_KEY"] + ) assert client.USE_MASTER_KEY is None leancloud.use_master_key(True) assert client.USE_MASTER_KEY is True leancloud.use_master_key(False) assert client.USE_MASTER_KEY is False - - -# def test_get_base_url(): -# leancloud.use_https(False) -# assert client.get_base_url().startswith('http://') -# leancloud.use_https(True) -# assert client.get_base_url().startswith('https://') - - -def test_get_server_time(): - assert type(client.get_server_time()) == datetime.datetime - - -def test_redirect_region(): - if client.REGION == 'US': - # US region server doesn't support app router now - return - # setup - old_app_router = client.app_router - client.app_router = AppRouter('test_app_id') - requests_intercept.install() - - def fake_app_router(environ, start_response): - assert environ['PATH_INFO'] == '/1/route' - start_response('200 OK', [('Content-Type', 'application/json')]) - return [json.dumps({ - 'api_server': 'fake-redirect-server', - 'ttl': 3600, - }).encode('utf-8')] - - host, port = 'app-router.leancloud.cn', 443 - add_wsgi_intercept(host, port, lambda: fake_app_router) - - def fake_redirect_server(environ, start_response): - start_response('307', [('Content-Type', 'application/json')]) - return [json.dumps({ - 'api_server': 'fake-api-server', - 'ttl': 3600, - }).encode('utf-8')] - - host, port = 'fake-redirect-server', 443 - add_wsgi_intercept(host, port, lambda: fake_redirect_server) - - - def fake_api_server(environ, start_response): - start_response('200', [('Content-Type', 'application/json')]) - return [json.dumps({ - 'result': 42, - }).encode('utf-8')] - - host, port = 'fake-api-server', 443 - add_wsgi_intercept(host, port, lambda: fake_api_server) - - # test - assert client.get('/redirectme').json()['result'] == 42 - - # teardown - client.app_router = old_app_router - requests_intercept.uninstall() diff --git a/tests/test_conversation.py b/tests/test_conversation.py new file mode 100644 index 00000000..b84bbaf2 --- /dev/null +++ b/tests/test_conversation.py @@ -0,0 +1,84 @@ +# coding: utf-8 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os + +from nose.tools import assert_equal + +import leancloud +from leancloud import Conversation, LeanCloudError + + +def setup(): + leancloud.client.USE_MASTER_KEY = None + leancloud.client.APP_ID = None + leancloud.client.APP_KEY = None + leancloud.client.MASTER_KEY = None + leancloud.init(os.environ["APP_ID"], master_key=os.environ["MASTER_KEY"]) + + +def test_create_conversation(): + conv = Conversation("testConversation") + conv.save() + assert conv.id + assert not conv.is_unique + assert not conv.is_system + assert not conv.is_transient + conv.destroy() + + conv = Conversation( + "testConversation", is_system=True, is_transient=True, is_unique=True + ) + conv.save() + assert conv.id + assert conv.is_system + assert conv.is_transient + assert not conv.is_unique + conv.destroy() + + conv = Conversation("testConversation", is_unique=True) + conv.save() + assert conv.id + assert conv.is_unique + conv.destroy() + + conv = Conversation("testConversation", is_unique=False) + conv.save() + assert conv.id + assert not conv.is_unique + conv.destroy() + + +def test_members(): + conv = Conversation("test") + conv.add_member("xxx") + conv.add_member("qqq") + conv.save() + + conv = Conversation.query.get(conv.id) + assert_equal(set(conv.members), set(["xxx", "qqq"])) + conv.add_member("aaa") + conv.save() + conv = Conversation.query.get(conv.id) + assert_equal(set(conv.members), set(["xxx", "qqq", "aaa"])) + conv.destroy() + + +def test_send(): + conv = Conversation("test") + conv.save() + conv.send("test_user", {"a": 1}) + conv.destroy() + + +def test_broadcast(): + conv = Conversation("test", is_system=True) + conv.save() + try: + conv.broadcast("system", {"b": 2}) + except LeanCloudError as e: + if e.code == 1 and e.error == "The daily quota of system messages is exceeded.": + pass diff --git a/tests/test_engine.py b/tests/test_engine.py index 5c9c87c6..8db3682f 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -8,51 +8,49 @@ import time import json import requests +import typing +from datetime import datetime -from wsgi_intercept import requests_intercept, add_wsgi_intercept - +import six +from nose.tools import assert_equal +from wsgi_intercept import requests_intercept +from wsgi_intercept import add_wsgi_intercept import leancloud from leancloud import Engine -from leancloud import cloudfunc +from leancloud import cloud from leancloud.engine import authorization +from leancloud.engine import leanengine from leancloud import LeanCloudError from .request_generator import generate_request -__author__ = 'asaka ' +__author__ = "asaka " -env = None +env = None # type: typing.Dict[str, str] -TEST_APP_ID = os.environ['APP_ID'] -TEST_APP_KEY = os.environ['APP_KEY'] -TEST_MASTER_KEY = os.environ['MASTER_KEY'] +TEST_APP_ID = os.environ["APP_ID"] +TEST_APP_KEY = os.environ["APP_KEY"] +TEST_MASTER_KEY = os.environ["MASTER_KEY"] +TEST_HOOK_KEY = os.environ["HOOK_KEY"] sign_by_app_key = generate_request(TEST_APP_KEY) sign_by_master_key = generate_request(TEST_MASTER_KEY, True) NORMAL_HEADERS = { - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, } def app(environ, start_response): - start_response('200 OK', [('Content-Type', 'text/plain')]) - return [b'Hello LeanCloud'] + start_response("200 OK", [("Content-Type", "text/plain")]) + return [b"Hello LeanCloud"] engine = Engine(app) - - -def make_app(): - return engine - - -host, port = 'localhost', 80 -url = 'http://{0}:{1}/'.format(host, port) - - -HookObject = leancloud.Object.extend('HookObject') +host, port = "localhost", 80 +url = "http://{0}:{1}/".format(host, port) +HookObject = leancloud.Object.extend("HookObject") def setup(): @@ -60,345 +58,653 @@ def setup(): leancloud.client.APP_ID = None leancloud.client.APP_KEY = None leancloud.client.MASTER_KEY = None - leancloud.init(TEST_APP_ID, TEST_APP_KEY, TEST_MASTER_KEY) + leancloud.init(TEST_APP_ID, TEST_APP_KEY, TEST_MASTER_KEY, TEST_HOOK_KEY) authorization._ENABLE_TEST = True authorization.APP_ID = TEST_APP_ID authorization.APP_KEY = TEST_APP_KEY authorization.MASTER_KEY = TEST_MASTER_KEY + authorization.HOOK_KEY = TEST_HOOK_KEY requests_intercept.install() - add_wsgi_intercept(host, port, make_app) + add_wsgi_intercept(host, port, lambda: engine) @engine.define def hello(**params): - return 'hello' + return "hello" + + @engine.define("fooBarBaz") + def foo_bar_baz(**params): + return "yes" def teardown(): requests_intercept.uninstall() -def test_origin_response(): +def test_ping(): # type: () -> None + response = requests.get( + url + "/__engine/1/ping", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + }, + ) + assert response.ok + + +def test_lean_engine_error(): + err = leancloud.LeanEngineError(status=404, code=1234567, message="nowhere") + assert err.status == 404 + assert err.code == 1234567 + assert err.message == "nowhere" + # backward compatibility tests + err = leancloud.LeanEngineError(code=2020, message="eanCloud") + assert err.status == 400 + assert err.code == 2020 + assert err.message == "eanCloud" + err = leancloud.LeanEngineError(233, "llllleancloud") + assert err.status == 400 + assert err.code == 233 + assert err.message == "llllleancloud" + err = leancloud.LeanEngineError(226, "leancloud") + assert err.status == 226 + assert err.code == 226 + assert err.message == "leancloud" + err = leancloud.LeanEngineError("error messages") + assert err.status == 400 + assert err.code == 400 + assert err.message == "error messages" + err = leancloud.LeanEngineError() + assert err.status == 400 + assert err.code == 400 + assert err.message == "error" + + +def test_origin_response(): # type: () -> None resp = requests.get(url) assert resp.ok - assert resp.content == b'Hello LeanCloud' + assert resp.content == b"Hello LeanCloud" -def test_compatibility(): - requests.get(url + '/1/functions/hello') - assert '_app_params' in authorization.current_environ +def test_compatibility(): # type: () -> None + requests.get(url + "/1/functions/hello") + assert "_app_params" in authorization.current_environ - requests.get(url + '/1.1/functions/hello') - assert '_app_params' in authorization.current_environ + requests.get(url + "/1.1/functions/hello") + assert "_app_params" in authorization.current_environ -def test_app_params_1(): - requests.get(url + '/__engine/1/functions/hello') - assert '_app_params' in authorization.current_environ +def test_app_params_1(): # type: () -> None + requests.get(url + "/__engine/1/functions/hello") + assert "_app_params" in authorization.current_environ -def test_app_params_2(): - requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-avoscloud-application-id': 'foo', - 'x-avoscloud-application-key': 'bar', - 'x-avoscloud-session-token': 'baz', - }) +def test_app_params_2(): # type: () -> None + requests.get( + url + "/__engine/1/functions/hello", + headers={ + "x-avoscloud-application-id": "foo", + "x-avoscloud-application-key": "bar", + "x-avoscloud-session-token": "baz", + }, + ) env = authorization.current_environ - assert env['_app_params']['id'] == 'foo' - assert env['_app_params']['key'] == 'bar' - assert env['_app_params']['session_token'] == 'baz' + assert env["_app_params"]["id"] == "foo" + assert env["_app_params"]["key"] == "bar" + assert env["_app_params"]["session_token"] == "baz" -def test_app_params_3(): - requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-avoscloud-request-sign': sign_by_app_key - }) +def test_app_params_3(): # type: () -> None + requests.get( + url + "/__engine/1/functions/hello", + headers={"x-avoscloud-request-sign": sign_by_app_key}, + ) env = authorization.current_environ - assert env['_app_params']['key'] == TEST_APP_KEY + assert env["_app_params"]["key"] == TEST_APP_KEY -def test_app_params_4(): - requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-avoscloud-request-sign': sign_by_master_key - }) +def test_app_params_4(): # type: () -> None + requests.get( + url + "/__engine/1/functions/hello", + headers={"x-avoscloud-request-sign": sign_by_master_key}, + ) env = authorization.current_environ - assert env['_app_params']['master_key'] == TEST_MASTER_KEY + assert env["_app_params"]["master_key"] == TEST_MASTER_KEY -def test_app_params_5(): - requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-avoscloud-application-id': 'foo', - 'x-avoscloud-master-key': 'bar', - }) +def test_app_params_5(): # type: () -> None + requests.get( + url + "/__engine/1/functions/hello", + headers={"x-avoscloud-application-id": "foo", "x-avoscloud-master-key": "bar"}, + ) env = authorization.current_environ - assert env['_app_params']['id'] == 'foo' - assert env['_app_params']['master_key'] == 'bar' + assert env["_app_params"]["id"] == "foo" + assert env["_app_params"]["master_key"] == "bar" -def test_short_app_params_1(): - requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-lc-id': 'foo', - 'x-lc-key': 'bar', - 'x-lc-session': 'baz', - }) - env = authorization.current_environ - assert env['_app_params']['id'] == 'foo' - assert env['_app_params']['key'] == 'bar' - assert env['_app_params']['master_key'] is None - assert env['_app_params']['session_token'] == 'baz' - - -def test_short_app_params_2(): - requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-lc-id': 'foo', - 'x-lc-key': 'bar,master', - 'x-lc-session': 'baz', - }) +def test_short_app_params_1(): # type: () -> None + requests.get( + url + "/__engine/1/functions/hello", + headers={"x-lc-id": "foo", "x-lc-key": "bar", "x-lc-session": "baz"}, + ) env = authorization.current_environ - assert env['_app_params']['id'] == 'foo' - assert env['_app_params']['key'] is None - assert env['_app_params']['master_key'] == 'bar' - assert env['_app_params']['session_token'] == 'baz' + assert env["_app_params"]["id"] == "foo" + assert env["_app_params"]["key"] == "bar" + assert env["_app_params"]["master_key"] is None + assert env["_app_params"]["session_token"] == "baz" -def test_short_app_params_3(): - requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-lc-sign': sign_by_app_key - }) +def test_short_app_params_2(): # type: () -> None + requests.get( + url + "/__engine/1/functions/hello", + headers={"x-lc-id": "foo", "x-lc-key": "bar,master", "x-lc-session": "baz"}, + ) env = authorization.current_environ - assert env['_app_params']['key'] == TEST_APP_KEY - assert env['_app_params']['master_key'] is None + assert env["_app_params"]["id"] == "foo" + assert env["_app_params"]["key"] is None + assert env["_app_params"]["master_key"] == "bar" + assert env["_app_params"]["session_token"] == "baz" -def test_short_app_params_4(): - requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-lc-sign': sign_by_master_key - }) - env = authorization.current_environ - assert env['_app_params']['key'] is None - assert env['_app_params']['master_key'] == TEST_MASTER_KEY - - -def test_body_params(): - requests.get(url + '/__engine/1/functions/hello', headers={ - 'Content-Type': 'text/plain', - }, data=json.dumps({ - '_ApplicationId': 'foo', - '_ApplicationKey': 'bar', - '_MasterKey': 'baz', - '_SessionToken': 'qux', - })) +def test_short_app_params_3(): # type: () -> None + requests.get( + url + "/__engine/1/functions/hello", headers={"x-lc-sign": sign_by_app_key} + ) env = authorization.current_environ - assert env['_app_params']['id'] == 'foo' - assert env['_app_params']['key'] == 'bar' - assert env['_app_params']['master_key'] == 'baz' - assert env['_app_params']['session_token'] == 'qux' + assert env["_app_params"]["key"] == TEST_APP_KEY + assert env["_app_params"]["master_key"] is None -def test_authorization_1(): - response = requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - }) +def test_short_app_params_4(): # type: () -> None + requests.get( + url + "/__engine/1/functions/hello", headers={"x-lc-sign": sign_by_master_key} + ) + env = authorization.current_environ + assert env["_app_params"]["key"] is None + assert env["_app_params"]["master_key"] == TEST_MASTER_KEY + + +def test_body_params(): # type: () -> None + requests.get( + url + "/__engine/1/functions/hello", + headers={"Content-Type": "text/plain"}, + data=json.dumps( + { + "_ApplicationId": "foo", + "_ApplicationKey": "bar", + "_MasterKey": "baz", + "_SessionToken": "qux", + } + ), + ) + env = authorization.current_environ + assert env["_app_params"]["id"] == "foo" + assert env["_app_params"]["key"] == "bar" + assert env["_app_params"]["master_key"] == "baz" + assert env["_app_params"]["session_token"] == "qux" + + +def test_authorization_1(): # type: () -> None + response = requests.get( + url + "/__engine/1/functions/hello", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + }, + ) assert response.ok - assert response.json() == {u'result': u'hello'} + assert response.json() == {u"result": u"hello"} -def test_authorization_2(): - response = requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-lc-id': TEST_APP_ID, - 'x-lc-key': TEST_MASTER_KEY, - }) +def test_authorization_2(): # type: () -> None + response = requests.get( + url + "/__engine/1/functions/hello", + headers={"x-lc-id": TEST_APP_ID, "x-lc-key": TEST_MASTER_KEY}, + ) assert response.ok - assert response.json() == {u'result': u'hello'} + assert response.json() == {u"result": u"hello"} -def test_authorization_3(): - response = requests.get(url + '/__engine/1/functions/hello', headers={ - 'x-avoscloud-application-id': 'foo', - 'x-avoscloud-application-key': 'bar', - }) +def test_authorization_3(): # type: () -> None + response = requests.get( + url + "/__engine/1/functions/hello", + headers={ + "x-avoscloud-application-id": "foo", + "x-avoscloud-application-key": "bar", + }, + ) assert response.status_code == 401 -def test_register_cloud_func(): +def test_register_cloud_func(): # type: () -> None @engine.define def ping(**params): - print('params:', params) assert params == {"foo": ["bar", "baz"]} - return 'pong' + return "pong" + + try: + + @engine.define("ping") + def duplicated_cloud_function_name(): + pass - response = requests.post(url + '/__engine/1/functions/ping', headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - }, json={'foo': ['bar', 'baz']}) + except RuntimeError: + pass + else: + raise AssertionError("registering same func_name isn't permitted.") + + response = requests.post( + url + "/__engine/1/functions/ping", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + }, + json={"foo": ["bar", "baz"]}, + ) assert response.ok - assert response.json() == {u'result': u'pong'} + assert response.json() == {u"result": u"pong"} # test run in local - assert cloudfunc.run.local('ping', foo=['bar', 'baz']) == 'pong' + assert cloud.run.local("ping", foo=["bar", "baz"]) == "pong" + +def test_cloud_func_name_alias(): # type: () -> None + response = requests.get( + url + "/__engine/1/functions/fooBarBaz", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + }, + ) + assert response.ok + assert response.json() == {u"result": u"yes"} -def test_rpc_call(): + +def test_realtime_cloud_func(): # type: () -> None @engine.define - def rpc(**params): - return leancloud.Object.create('Xxx', foo=['bar', 'baz']) + def _messageReceived(): + pass - obj = cloudfunc.rpc.local('rpc') + try: + cloud.run.local("_messageReceived") + except leancloud.LeanEngineError as e: + assert_equal(e.status, 401) + assert_equal(e.code, 401) + else: + raise AssertionError + response = requests.post( + url + "/__engine/1/functions/_messageReceived", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + }, + json={"foo": ["bar", "baz"], "__sign": "123,xxx"}, + ) + assert response.status_code == 401 + + +def test_on_verified(): # type: () -> None + @engine.on_verified("sms") + def on_sms_verified(user): + assert isinstance(user, leancloud.User) + assert user.id == "xxx" + + response = requests.post( + url + "/__engine/1/functions/onVerified/sms", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": "invalid-hook-key", + }, + json={"object": {"__sign": "123,xxx"}}, + ) + assert_equal(response.status_code, 401) + response = requests.post( + url + "/__engine/1/functions/onVerified/sms", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + json={"object": {"objectId": "xxx"}}, + ) + assert response.ok + + +def test_rpc_call(): # type: () -> None + # test unsaved object + @engine.define + def rpc_unsaved(**params): + result = leancloud.Object.create("Xxx", foo=["bar", "baz"]) + return result + + obj = cloud.rpc.local("rpc_unsaved") assert isinstance(obj, leancloud.Object) - assert obj.get('foo') == ['bar', 'baz'] + assert obj.get("foo") == ["bar", "baz"] + # tewst saved object + @engine.define + def rpc_saved(**params): + result = leancloud.Object.create("Xxx", foo=["bar", "baz"]) + result.save() + return result + + obj = cloud.rpc.local("rpc_saved") + assert isinstance(obj, leancloud.Object) + assert obj.get("foo") == ["bar", "baz"] + obj.destroy() -def test_before_save_hook(): - @engine.before_save('HookObject') + # test object list + @engine.define + def rpc_list(**params): + objs = [ + leancloud.Object.create("Xxx", foo=["bar"]), + leancloud.Object.create("xXX", foo=["baz"]), + ] + objs[0].save() + return objs + + objs = cloud.rpc.local("rpc_list") + assert isinstance(objs, list) + assert objs[0].get("foo") == ["bar"] + assert objs[1].get("foo") == ["baz"] + objs[0].destroy() + + +def test_before_save_hook(): # type: () -> None + @engine.before_save("HookObject") def before_hook_object_save(obj): - assert obj.has('__before') - obj.set('beforeSaveHookInserted', True) - - response = requests.post(url + '/__engine/1/functions/HookObject/beforeSave', json={ - 'object': {'clientValue': 'blah'} - }, headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - }) + obj.set("beforeSaveHookInserted", True) + + response = requests.post( + url + "/__engine/1/functions/HookObject/beforeSave", + json={"object": {"clientValue": "blah"}}, + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + ) assert response.ok - assert response.json()['beforeSaveHookInserted'] == True - assert response.json()['clientValue'] == 'blah' - assert '__before' in response.json() + assert response.json()["beforeSaveHookInserted"] is True + assert response.json()["clientValue"] == "blah" -def test_after_save_hook(): - @engine.after_save('HookObject') +def test_after_save_hook(): # type: () -> None + @engine.after_save("HookObject") def after_hook_object_save(obj): - assert obj.has('__after') - - response = requests.post(url + '/__engine/1/functions/HookObject/afterSave', json={ - 'object': {'clientValue': 'blah'} - }, headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - }) + pass + + response = requests.post( + url + "/__engine/1/functions/HookObject/afterSave", + json={"object": {"clientValue": "blah"}}, + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + ) assert response.ok - assert response.json() == {'result': 'ok'} + assert response.json() == {"result": "ok"} -def test_before_update_hook(): - @engine.before_update('HookObject') +def test_before_update_hook(): # type: () -> None + @engine.before_update("HookObject") def before_hook_object_update(obj): - assert obj.updated_keys == ['clientValue'] - - response = requests.post(url + '/__engine/1/functions/HookObject/beforeUpdate', json={ - 'object': {'clientValue': 'blah', '__updateKeys': ['clientValue']} - }, headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - }) + assert obj.updated_keys == ["clientValue"] + + response = requests.post( + url + "/__engine/1/functions/HookObject/beforeUpdate", + json={"object": {"clientValue": "blah", "_updatedKeys": ["clientValue"]}}, + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + ) assert response.ok -def test_before_delete_hook(): - @engine.before_delete('HookObject') +def test_before_delete_hook(): # type: () -> None + @engine.before_delete("HookObject") def before_hook_object_delete(obj): pass - response = requests.post(url + '/__engine/1/functions/HookObject/beforeDelete', json={ - 'object': {} - }, headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - }) + response = requests.post( + url + "/__engine/1/functions/HookObject/beforeDelete", + json={"object": {}}, + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + ) assert response.ok assert response.json() == {} -def test_on_login(): +def test_on_login(): # type: () -> None @engine.on_login def on_login(user): assert isinstance(user, leancloud.User) - response = requests.post(url + '/__engine/1.1/functions/_User/onLogin', json={ - 'object': {} - }, headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - }) + response = requests.post( + url + "/__engine/1.1/functions/_User/onLogin", + json={"object": {}}, + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + ) + assert response.ok + + +def test_on_auth_data(): # type: () -> None + @engine.on_auth_data + def on_auth_data(auth_data): + if auth_data["foo"]["openid"] == "openid": + auth_data["foo"]["uid"] = "openid" + return auth_data + + response = requests.post( + url + "/__engine/1.1/functions/_User/onAuthData", + json={"authData": { + "foo": { + "openid": "openid", + "access_token": "access_token", + "expires_in": 123456789, + } + }}, + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + ) assert response.ok + assert response.json()["result"]["foo"]["uid"] == "openid" -def test_bigquery(): - @engine.on_bigquery('end') - def on_bigquery_end(ok, data): +def test_insight(): # type: () -> None + @engine.on_insight("end") + def on_insight_end(ok, data): assert ok is False assert data == { "id": u"job id", "status": u"OK/ERROR", - "message": u"当 status 为 ERROR 时的错误消息" + "message": u"当 status 为 ERROR 时的错误消息", } - response = requests.post(url + '/__engine/1/functions/BigQuery/onComplete', headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - }, json={ - "id": u"job id", - "status": u"OK/ERROR", - "message": u"当 status 为 ERROR 时的错误消息" - }) + response = requests.post( + url + "/__engine/1/functions/BigQuery/onComplete", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + json={ + "id": u"job id", + "status": u"OK/ERROR", + "message": u"当 status 为 ERROR 时的错误消息", + }, + ) assert response.ok -def test_client(): - leancloud.init(os.environ['APP_ID'], os.environ['APP_KEY']) - assert cloudfunc.run('add', a=1, b=2) == 3 +def test_client(): # type: () -> None + assert cloud.run("add", a=1, b=2) == 3 -def test_request_sms_code(): - if leancloud.client.REGION == 'US': - return - leancloud.init(os.environ['APP_ID'], master_key=os.environ['MASTER_KEY']) +def test_request_sms_code(): # type: () -> None try: - cloudfunc.request_sms_code('13111111111') + # numbers come from http://www.z-sms.com/ + cloud.request_sms_code("+8617180655340") + time.sleep(60) + cloud.request_sms_code("+447365753569") + time.sleep(60) + cloud.request_sms_code("17180655340") + time.sleep(60) + cloud.request_sms_code("7365753569", idd="+44") + time.sleep(60) + cloud.request_sms_code("17180655340", idd="+86") + time.sleep(60) + cloud.request_sms_code("+447365753569", idd="+44") + time.sleep(60) + cloud.request_sms_code("+8617180655340", idd="+44") # +8617180655340 + time.sleep(60) + cloud.request_sms_code("+447365753569", idd="+86") # +447365753569 except LeanCloudError as e: - # 短信发送过于频繁或者欠费或者关闭短信功能 - if e.code in (601, 160, 119): + if e.code in (605, 160, 119): # unverified template, insufficient balance, sms service disabled + pass + elif e.code == 601 or e.error.startswith("SMS request too fast"): # send sms too frequently + pass + elif "SMS sending exceeds limit" in e.error: + pass + elif "send too frequently" in e.error: pass else: raise e -def test_current_user(): - leancloud.init(os.environ['APP_ID'], master_key=os.environ['MASTER_KEY']) +def test_get_server_time(): # type: () -> None + assert type(leancloud.client.get_server_time()) == datetime + + +def test_captcha(): # type: () -> None + if leancloud.client.REGION == "US": + return + if leancloud.client.APP_ID.endswith("-9Nh9j0Va"): + return + try: + captcha = cloud.request_captcha(size=3, height=100) + except LeanCloudError as e: + assert_equal(e.code, 119) # captcha flag is disabled + return + assert captcha.token + assert captcha.url + return + # test captcha by human: + print(captcha.url) + code = six.moves.input("code: ") + result = captcha.verify(code) + print(result) + + +def test_current_user(): # type: () -> None saved_user = leancloud.User() - saved_user.set('username', 'user{0}'.format(int(time.time()))) - saved_user.set('password', 'password') - saved_user.set_email('{0}@leancloud.rocks'.format(int(time.time()))) + saved_user.set("username", "user{0}".format(int(time.time()))) + saved_user.set("password", "password") + saved_user.set_email("{0}@leancloud.rocks".format(int(time.time()))) saved_user.sign_up() session_token = saved_user.get_session_token() @engine.define def current_user(): - user = engine.current_user - TestCurrentUser = leancloud.Object.extend('TestCurrentUser') + user = engine.current.user + TestCurrentUser = leancloud.Object.extend("TestCurrentUser") o = TestCurrentUser() - o.set('user', user) - o.set({'yetAnotherUser': user}) + o.set("user", user) + o.set({"yetAnotherUser": user}) o.save() - TestCurrentUser.query.equal_to('user', user).find() - assert user.get('username') == saved_user.get('username') - - response = requests.get(url + '/__engine/1/functions/current_user', headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - 'x-avoscloud-session-token': session_token, - }) + TestCurrentUser.query.equal_to("user", user).find() + assert user.get("username") == saved_user.get("username") + + # test current + assert engine.current.session_token == session_token + assert user.get("username") is engine.current.user.get("username") + assert "remote_address" in engine.current.meta + + response = requests.get( + url + "/__engine/1/functions/current_user", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-avoscloud-session-token": session_token, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + ) assert response.status_code == 200 - @engine.before_save('Xxx') + @engine.before_save("Xxx") def before_xxx_save(xxx): - assert engine.current_user.get('username') == saved_user.get('username') + assert engine.current.user.get("username") == saved_user.get("username") - response = requests.post(url + '/__engine/1/functions/Xxx/beforeSave', headers={ - 'x-avoscloud-application-id': TEST_APP_ID, - 'x-avoscloud-application-key': TEST_APP_KEY, - }, json={'object': {}, 'user': {'username': saved_user.get('username')}}) - assert response.status_code == 200 + response = requests.post( + url + "/__engine/1/functions/Xxx/beforeSave", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + "x-lc-hook-key": TEST_HOOK_KEY, + }, + json={"object": {}, "user": {"username": saved_user.get("username")}}, + ) + assert_equal(response.status_code, 200) + + # cleanup + saved_user.destroy() + + +def test_engine_register(): # type: () -> None + temp_engine = Engine() + + @temp_engine.define + def testing(): + return "testing" + + engine.register(temp_engine) + + try: + engine.register(temp_engine) # check if it will raise RuntimeError + except RuntimeError: + pass + else: + raise AssertionError("check if it will raise RuntimeError") + + response = requests.post( + url + "/__engine/1/functions/testing", + headers={ + "x-avoscloud-application-id": TEST_APP_ID, + "x-avoscloud-application-key": TEST_APP_KEY, + }, + ) + assert response.ok + + +def test_engine_wrap(): # type: () -> None + def temp_app(environ, start_response): + start_response("200 OK", [("Content-Type", "text/plain")]) + return [b"testing"] + + try: + engine.wrap(temp_app) + except RuntimeError: + pass + else: + raise AssertionError("rewriting wsgi_app isn't permitted.") + leanengine.root_engine = None # for passing RuntimeError. + engine.wrap(temp_app) + response = requests.get(url) + assert response.ok + assert response.content == b"testing" diff --git a/tests/test_file.py b/tests/test_file.py index 5de30fc4..158666b1 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -5,92 +5,201 @@ from __future__ import print_function import os +import io -from nose.tools import with_setup -from nose.tools import assert_raises -from nose.tools import raises +import six + +if six.PY2: + from urlparse import urlparse +if six.PY3: + from urllib.parse import urlparse import requests +from nose.tools import with_setup # type: ignore +from nose.tools import assert_raises # type: ignore +from nose.tools import raises # type: ignore import leancloud -from leancloud import File +from leancloud import File, LeanCloudError from leancloud import ACL -from leancloud._compat import PY2 -from leancloud._compat import BytesIO -from leancloud._compat import buffer_type -__author__ = 'asaka' +__author__ = "asaka" def setup_func(): - leancloud.init( - os.environ['APP_ID'], - master_key=os.environ['MASTER_KEY'] - ) + leancloud.init(os.environ["APP_ID"], master_key=os.environ["MASTER_KEY"]) -def test_basic(): - s = BytesIO(b'blah blah blah') - if PY2: - import cStringIO - s1 = cStringIO.StringIO() - s1.write('blah blah blah') - else: - s1 = s - f1 = File('Blah', s, 'text/plain') - f2 = File('Blah', s1) - for f in (f1, f2): - assert f.name == 'Blah' - assert f._metadata['size'] == 14 +def setup_without_master_key(): + leancloud.init(os.environ["APP_ID"], os.environ["APP_KEY"]) + + +def test_basic(): # type: () -> None + def fn(s): + f = File("Blah", s, mime_type="text/plain") + assert f.name == "Blah" + assert f._metadata["size"] == 14 assert f.size == 14 - assert f._type == 'text/plain' + assert f._metadata["_checksum"] == "55e562bfee2bde4f9e71b8885eb5e303" + + b = b"blah blah blah" + fn(io.BytesIO(b)) + fn(memoryview(b)) + + import tempfile + + f = tempfile.SpooledTemporaryFile() + f.write(b) + fn(f) + if six.PY2: + import StringIO + import cStringIO + + fn(StringIO.StringIO(b)) + fn(cStringIO.StringIO(b)) + fn(buffer(b)) # noqa: F821 + + if six.PY3: + fn(b) + +def test_create_with_url(): # type: () -> None + f = File.create_with_url( + "xxx", + u"http://i1.wp.com/leancloud.cn/images/static/default-avatar.png", + meta_data={}, + ) + assert f._url == "http://i1.wp.com/leancloud.cn/images/static/default-avatar.png" + assert f.url is None -def test_create_with_url(): - f = File.create_with_url('xxx', 'http://i1.wp.com/leancloud.cn/images/static/default-avatar.png', meta_data={}) - assert f.url == 'http://i1.wp.com/leancloud.cn/images/static/default-avatar.png' +def test_create_without_data(): # type: () -> None + f = File.create_without_data("a123") + assert f.id == "a123" -def test_create_without_data(): - f = File.create_without_data('a123') - assert f.id == 'a123' +def test_acl(): # type: () -> None + acl_ = ACL() + f = File("Blah", io.BytesIO(b"xxx")) + assert_raises(TypeError, f.set_acl, "a") + f.set_acl(acl_) + assert f.get_acl() == acl_ + + +@with_setup(setup_func) +def test_save(): # type: () -> None + user = leancloud.User() + name = "user1_name" + passwd = "password" + try: + user.login(name, passwd) + except LeanCloudError as e: + if e.code == 211: + user.set_username(name) + user.set_password(passwd) + user.sign_up() + user.login(name, passwd) + + f = File("Blah.txt", open("tests/sample_text.txt", "rb")) + f.save() -def test_acl(): - acl = ACL() - f = File('Blah', buffer_type(b'xxx')) - assert_raises(TypeError, f.set_acl, 'a') - f.set_acl(acl) - assert f.get_acl() == acl + assert f.owner_id == user.id + assert f.id + assert f.name == "Blah.txt" + assert f.mime_type == "text/plain" + assert not f.url.endswith(".") + assert f.created_at == f.updated_at @with_setup(setup_func) -def test_save(): - f = File('Blah', buffer_type(b'xxx')) +def test_save_with_specified_key(): # type: () -> None + f = File("Blah.txt", open("tests/sample_text.txt", "rb")) + user_specified_key = "abc" + f.key = user_specified_key f.save() + assert f.id - assert f.name == 'Blah' + assert f.created_at == f.updated_at + assert f.name == "Blah.txt" + assert f.mime_type == "text/plain" + path = urlparse(f.url).path + if path.startswith("/avos-cloud-"): # old school aws s3 file url + assert path.split("/")[2] == user_specified_key + elif f.url.startswith("https://lc-gluttony"): # new aws s3 gluttony bucket + gluttony_path = "/" + os.environ["APP_ID"][0:12] + "/" + user_specified_key + assert path == gluttony_path + else: + assert path == "/" + user_specified_key + + +@with_setup(setup_without_master_key) +def test_save_with_specified_key_but_without_master_key(): # type: () -> None + f = File("Blah.txt", open("tests/sample_text.txt", "rb")) + f.key = "abc" + try: + f.save() + except LeanCloudError as e: + if e.code == 1 and e.error.startswith("Unsupported file key"): + pass @with_setup(setup_func) -def test_save_external(): - f = File.create_with_url('lenna.jpg', 'http://i1.wp.com/leancloud.cn/images/static/default-avatar.png') +def test_query(): # type: () -> None + files = leancloud.Query("File").find() + for f in files: + assert isinstance(f, File) + assert f.id + assert f.url + assert f.name + assert f.metadata + assert f.created_at == f.updated_at + if f.metadata.get("__source") == 'external': + assert f.url + else: + assert f.key + + assert isinstance(leancloud.File.query.first(), File) + +@with_setup(setup_func) +def test_scan(): # type: () -> None + files = leancloud.Query("File").scan() + for f in files: + assert isinstance(f, File) + assert f.created_at == f.updated_at + assert f.name + assert f.metadata + if f.metadata.get("__source") == 'external': + assert f.url + else: + assert f.key + + +@with_setup(setup_func) +def test_save_external(): # type: () -> None + file_name = "lenna.jpg" + file_url = "http://i1.wp.com/leancloud.cn/images/static/default-avatar.png" + f = File.create_with_url(file_name, file_url) f.save() assert f.id + assert f.created_at == f.updated_at + file_on_cloud = File.create_without_data(f.id) + file_on_cloud.fetch() + assert file_on_cloud.name == file_name + assert file_on_cloud.url == file_url @raises(ValueError) -def test_thumbnail_url_erorr(): - f = File.create_with_url('xx', '') +def test_thumbnail_url_erorr(): # type: () -> None + f = File.create_with_url("xx", "") f.get_thumbnail_url(100, 100) @with_setup(setup_func) @raises(ValueError) -def test_thumbnail_size_erorr(): - r = requests.get('http://i1.wp.com/leancloud.cn/images/static/default-avatar.png') - b = buffer_type(r.content) - f = File('Lenna2.jpg', b) +def test_thumbnail_size_erorr(): # type: () -> None + r = requests.get("http://i1.wp.com/leancloud.cn/images/static/default-avatar.png") + b = io.BytesIO(r.content) + f = File("Lenna2.jpg", b) f.save() assert f.id @@ -99,45 +208,67 @@ def test_thumbnail_size_erorr(): @with_setup(setup_func) -def test_thumbnail(): - r = requests.get('http://i1.wp.com/leancloud.cn/images/static/default-avatar.png') - b = buffer_type(r.content) - f = File('Lenna2.jpg', b) +def test_thumbnail(): # type: () -> None + r = requests.get("http://i1.wp.com/leancloud.cn/images/static/default-avatar.png") + b = io.BytesIO(r.content) + f = File("Lenna2.jpg", b) f.save() assert f.id url = f.get_thumbnail_url(100, 100) - assert url.endswith('?imageView/2/w/100/h/100/q/100/format/png') + assert url.endswith("?imageView/2/w/100/h/100/q/100/format/png") @with_setup(setup_func) -def test_destroy(): - r = requests.get('http://i1.wp.com/leancloud.cn/images/static/default-avatar.png') - b = buffer_type(r.content) - f = File('Lenna2.jpg', b) +def test_destroy(): # type: () -> None + r = requests.get("http://i1.wp.com/leancloud.cn/images/static/default-avatar.png") + b = io.BytesIO(r.content) + f = File("Lenna2.jpg", b) f.save() assert f.id f.destroy() @with_setup(setup_func) -def test_fetch(): - r = requests.get('http://i1.wp.com/leancloud.cn/images/static/default-avatar.png') - b = buffer_type(r.content) - f = File('Lenna2.jpg', b) - f.metadata['foo'] = 'bar' +def test_file_callback(): # type: () -> None + d = {} + + def noop(token, *args, **kwargs): + d["token"] = token + + f = File("xxx", io.BytesIO(b"xxx")) + f._save_to_s3 = noop + f._save_to_qiniu = noop + f._save_to_qcloud = noop + f.save() + f._save_callback(d["token"], False) + + # time.sleep(3) + # File should be deleted by API server + # assert_raises(leancloud.LeanCloudError, File.query().get, f.id) + + +@with_setup(setup_func) +def test_fetch(): # type: () -> None + r = requests.get("http://i1.wp.com/leancloud.cn/images/static/default-avatar.png") + b = io.BytesIO(r.content) + f = File("Lenna2.jpg", b) + f.metadata["foo"] = "bar" f.save() fetched = File.create_without_data(f.id) fetched.fetch() + + normalized_f_url = f.url.split("/")[-1] + normalized_fetched_url = f.url.split("/")[-1] + assert fetched.id == f.id assert fetched.metadata == f.metadata assert fetched.name == f.name - assert fetched.url == f.url assert fetched.size == f.size - assert fetched.url == f.url + assert fetched.url == f.url or normalized_fetched_url == normalized_f_url f.destroy() -def test_checksum(): - f = File('Blah', open('tests/sample_text.txt', 'rb')) - assert f._metadata['_checksum'] == 'd0588d95e45eed70745ffabdf0b18acd' +def test_checksum(): # type: () -> None + f = File("Blah", open("tests/sample_text.txt", "rb")) + assert f._metadata["_checksum"] == "d0588d95e45eed70745ffabdf0b18acd" diff --git a/tests/test_geo_point.py b/tests/test_geo_point.py index 457ddce5..58b45d68 100644 --- a/tests/test_geo_point.py +++ b/tests/test_geo_point.py @@ -4,21 +4,21 @@ from __future__ import division from __future__ import print_function -from nose.tools import assert_raises -from nose.tools import eq_ +from nose.tools import assert_raises # type: ignore +from nose.tools import eq_ # type: ignore from leancloud import GeoPoint -__author__ = 'asaka ' +__author__ = "asaka " -def test_invalid(): +def test_invalid(): # type: () -> None assert_raises(ValueError, GeoPoint, 0, 200) assert_raises(ValueError, GeoPoint, -100, 10) -def test_setter(): +def test_setter(): # type: () -> None p = GeoPoint() p.latitude = 10 assert p.latitude == 10 @@ -29,16 +29,19 @@ def f(): assert_raises(ValueError, f) -def test_dump(): +def test_dump(): # type: () -> None p = GeoPoint(10, 20) - assert p.dump() == {'latitude': 10, '__type': 'GeoPoint', 'longitude': 20} + assert p.dump() == {"latitude": 10, "__type": "GeoPoint", "longitude": 20} -def test_radians_to(): +def test_radians_to(): # type: () -> None assert GeoPoint(0, 0).radians_to(GeoPoint(10, 10)) - 0.24619691677893205 < 0.00001 - assert GeoPoint(10, 10).radians_to(GeoPoint(14.5, 24.5)) - 0.25938444522905957 < 0.00001 + assert ( + GeoPoint(10, 10).radians_to(GeoPoint(14.5, 24.5)) - 0.25938444522905957 + < 0.00001 + ) -def test_eq(): +def test_eq(): # type: () -> None eq_(GeoPoint(0, 1) == GeoPoint(0, 1), True) eq_(GeoPoint(1, 1) == GeoPoint(1, 0), False) diff --git a/tests/test_message.py b/tests/test_message.py new file mode 100644 index 00000000..1bd48aaf --- /dev/null +++ b/tests/test_message.py @@ -0,0 +1,59 @@ +# coding: utf-8 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import os +import time + +from nose.tools import assert_equal +from nose.tools import assert_true + +import leancloud +from leancloud import Message +from leancloud import Conversation + + +def setup(): + leancloud.init(os.environ["APP_ID"], master_key=os.environ["MASTER_KEY"]) + + +def test_message_find_by_conversation(): + conv = Conversation(name="test") + conv.save() + conv.send("foo", "what the hell") + time.sleep(2) # wait for server sync + msgs = Message.find_by_conversation(conv.id, limit=1000, reversed=False) + assert_equal(len(msgs), 1) + msg = msgs[0] + assert_equal(msg.bin, False) + assert_equal(msg.conversation_id, conv.id) + assert_equal(msg.data, "what the hell") + assert_equal(msg.from_client, "foo") + assert_equal(msg.is_conversation, True) + assert_equal(msg.is_room, False) + assert_true(msg.message_id) + assert_equal(msg.to, conv.id) + conv.destroy() + + +def test_message_find_by_client(): + conv = Conversation(name="test") + conv.save() + conv.send("foo", "what the hell") + time.sleep(1) # wait for server sync + msgs = Message.find_by_client("foo", limit=123) + assert len(msgs) > 0 + conv.destroy() + + +def test_message_find_all(): + conv = Conversation(name="test") + conv.save() + conv.send("foo", "what the hell") + time.sleep(1) # wait for server sync + msgs = Message.find_all(limit=1000) + assert len(msgs) > 0 + conv.destroy() diff --git a/tests/test_middlewares.py b/tests/test_middlewares.py new file mode 100644 index 00000000..1551e14f --- /dev/null +++ b/tests/test_middlewares.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import flask +import requests +from wsgi_intercept import add_wsgi_intercept +from wsgi_intercept import remove_wsgi_intercept +from wsgi_intercept import requests_intercept + +from leancloud import user as user_module +from leancloud.engine.cookie_session import CookieSessionMiddleware +from leancloud.engine import https_redirect_middleware + +HOST, PORT = "localhost", 80 +URL = "http://{}:{}/".format(HOST, PORT) +FAKE_USER_DATA = { + "sessionToken": "qmdj8pdidnmyzp0c7yqil91oc", + "updatedAt": "2015-07-14T02:31:50.100Z", + "phone": "18612340000", + "objectId": "55a47496e4b05001a7732c5f", + "username": "fool", + "createdAt": "2015-07-14T02:31:50.100Z", + "emailVerified": False, + "mobilePhoneVerified": False, +} + +application = flask.Flask("test_app") + + +@application.route("/") +def route_index(): + return "hello" + + +@application.route("/logout") +def route_logout(): + user = user_module.User.get_current() + user.logout() + return "ok" + + +def setup(): + requests_intercept.install() + + +def teardown(): + requests_intercept.uninstall() + + +def test_cookie_session_middleware(): + user = user_module.User() + user._update_data(FAKE_USER_DATA) + user_module.thread_locals.current_user = user + + app = CookieSessionMiddleware(application, b"wtf!", expires=3600, max_age=3600) + add_wsgi_intercept(HOST, PORT, lambda: app) + + response = requests.get(URL) + assert response.cookies["leancloud:session"] + + del user_module.thread_locals.current_user + requests.get(URL, cookies=response.cookies) + current = user_module.User.get_current() + assert current.id == user.id + assert current.get_session_token() == user.get_session_token() + assert not current._attributes + + del user_module.thread_locals.current_user + response = requests.get(URL + "/logout", cookies=response.cookies) + assert "leancloud:session" not in response.cookies + + # TODO: try not using for..in to get cookie + for cookie in response.cookies: + if cookie.name == "leancloud:session": + assert cookie.expires + assert cookie.max_age + break + + remove_wsgi_intercept() + + +def test_https_redirect_middleware(): + https_redirect_middleware.is_prod = True + app = https_redirect_middleware.HttpsRedirectMiddleware(application) + add_wsgi_intercept(HOST, PORT, lambda: app) + + response = requests.get(url=URL, allow_redirects=False) + + assert response.is_redirect is True + assert response.next.url[:5] == "https" + + remove_wsgi_intercept() diff --git a/tests/test_object.py b/tests/test_object.py index f4d413c6..952a3cf2 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -5,17 +5,20 @@ from __future__ import print_function import os +from datetime import datetime -from nose.tools import with_setup -from nose.tools import ok_ -from nose.tools import eq_ -from nose.tools import assert_raises +from dateutil import tz +from nose.tools import assert_equal # type: ignore +from nose.tools import assert_raises # type: ignore +from nose.tools import eq_ # type: ignore +from nose.tools import ok_ # type: ignore +from nose.tools import with_setup # type: ignore import leancloud from leancloud import Object from leancloud import Query -__author__ = 'asaka ' +__author__ = "asaka " def setup_func(): @@ -23,10 +26,7 @@ def setup_func(): leancloud.client.APP_ID = None leancloud.client.APP_KEY = None leancloud.client.MASTER_KEY = None - leancloud.init( - os.environ['APP_ID'], - os.environ['APP_KEY'] - ) + leancloud.init(os.environ["APP_ID"], os.environ["APP_KEY"]) class Album(Object): @@ -37,28 +37,28 @@ class Band(Object): pass -def test_new(): +def test_new(): # type: () -> None album = Album() - assert album._class_name == 'Album' + assert album._class_name == "Album" -def test_class_equal(): - AnotherAlbum = Object.extend('Album') +def test_class_equal(): # type: () -> None + AnotherAlbum = Object.extend("Album") assert AnotherAlbum is Album album = AnotherAlbum() assert isinstance(album, AnotherAlbum) -def test_dirty(): +def test_dirty(): # type: () -> None album = Album() assert album.is_dirty() is True - album.id = 123 + album.id = "123" assert album.is_dirty() is False - album.set('foo', 'bar') + album.set("foo", "bar") assert album.is_dirty() is True -def test_find_unsaved_children(): +def test_find_unsaved_children(): # type: ignore album = Album() unsaved_children = [] unsaved_files = [] @@ -67,206 +67,235 @@ def test_find_unsaved_children(): assert unsaved_files == [] -def test_find_unsaved_children_2(): +def test_find_unsaved_children_2(): # type: ignore album = Album() band = Band() - album.set('band', band) + album.set("band", band) unsaved_children = [] unsaved_files = [] Object._find_unsaved_children(album, unsaved_children, unsaved_files) assert unsaved_children == [band, album] -def test_set(): +def test_set(): # type: () -> None album = Album() - album.set('title', 'Nightwish') - eq_(album._attributes, {'title': 'Nightwish'}) + album.set("title", "Nightwish") + eq_(album._attributes, {"title": "Nightwish"}) - album = Album(title='Nightwish') - eq_(album._attributes, {'title': 'Nightwish'}) + album = Album(title="Nightwish") + eq_(album._attributes, {"title": "Nightwish"}) -def test_get(): +def test_get(): # type: () -> None album = Album() - album.set('foo', 'bar') - assert album.get('foo') == 'bar' + album.set("foo", "bar") + assert album.get("foo") == "bar" -def test_get_deafult(): +def test_get_default(): # type: () -> None album = Album() - assert album.get('foo', 'bar') == 'bar' + assert album.get("foo", "bar") == "bar" + assert album.get("foo", default="bar") == "bar" + # for backward compatibility + assert album.get("foo", deafult="bar") == "bar" + assert album.get("foo", "bar", deafult="foobar") == "bar" + assert album.get("foo", deafult="foobar", default="bar") == "bar" -def test_unset(): +def test_unset(): # type: () -> None album = Album() - album.set('foo', 'bar') - album.unset('foo') - assert album.get('foo') is None - assert album.has('foo') is False + album.set("foo", "bar") + album.unset("foo") + assert album.get("foo") is None + assert album.has("foo") is False -def test_increment(): +def test_increment(): # type: () -> None album = Album() - album.set('foo', 1) - album.increment('foo', 1) - assert album.get('foo') == 2 + album.set("foo", 1) + album.increment("foo", 1) + assert album.get("foo") == 2 @with_setup(setup_func) -def test_increment_atfer_save(): +def test_bit_operation(): # type: () -> None album = Album() - album.set('foo', 1) + album.set("flags", 0b0) + album.bit_and("flags", 0b1) + assert_equal(album.get("flags"), 0b0) album.save() - album.increment('foo', 234) - assert album.get('foo') == 235 + assert_equal(album.get("flags"), 0b0) + + album.bit_or("flags", 0b1) + assert_equal(album.get("flags"), 0b1) + album.save() + assert_equal(album.get("flags"), 0b1) + + album.bit_xor("flags", 0b10) + assert_equal(album.get("flags"), 0b11) album.save() - assert album.get('foo') == 235 + assert_equal(album.get("flags"), 0b11) -def test_add(): +@with_setup(setup_func) +def test_increment_atfer_save(): # type: () -> None album = Album() - album.add('foo', 1) - eq_(album.get('foo'), [1]) - album.add('foo', 2) - eq_(album.get('foo'), [1, 2]) + album.set("foo", 1) + album.save() + album.increment("foo", 234) + assert album.get("foo") == 235 + album.save() + assert album.get("foo") == 235 -def test_add_unique(): +def test_add(): # type: () -> None album = Album() - album.add_unique('foo', 1) - album.add_unique('foo', 1) - eq_(album.get('foo'), [1]) - album.add_unique('foo', 2) - eq_(album.get('foo'), [1, 2]) + album.add("foo", 1) + eq_(album.get("foo"), [1]) + album.add("foo", 2) + eq_(album.get("foo"), [1, 2]) -def test_remove(): +def test_add_unique(): # type: () -> None album = Album() - album.set('foo', ['bar', 'baz']) - album.remove('foo', 'bar') - eq_(album.get('foo'), ['baz']) + album.add_unique("foo", 1) + album.add_unique("foo", 1) + eq_(album.get("foo"), [1]) + album.add_unique("foo", 2) + eq_(album.get("foo"), [1, 2]) -def test_clear(): +def test_remove(): # type: () -> None + album = Album() + album.set("foo", ["bar", "baz"]) + album.remove("foo", "bar") + eq_(album.get("foo"), ["baz"]) + + +def test_clear(): # type: () -> None album = Album(foo=1, bar=2, baz=3) album.clear() - assert album.get('foo') is None - assert album.get('bar') is None - assert album.get('baz') is None + assert album.get("foo") is None + assert album.get("bar") is None + assert album.get("baz") is None -def test_full_dump(): +def test_full_dump(): # type: ignore album = Album() - album.set('title', 'Nightwish') + album.set("title", "Nightwish") assert album._dump() == { - 'className': 'Album', - '__type': 'Object', - 'title': 'Nightwish', + "className": "Album", + "__type": "Object", + "title": "Nightwish", } band = Band() - album.set('band', band) + album.set("band", band) assert album._dump() == { - 'className': 'Album', - 'band': { - 'className': 'Band', - '__type': 'Pointer', - 'objectId': None, - }, - '__type': 'Object', - 'title': 'Nightwish' + "className": "Album", + "band": {"className": "Band", "__type": "Pointer", "objectId": None}, + "__type": "Object", + "title": "Nightwish", } -def test_dump_save(): +def test_dump_save(): # type: ignore album = Album() - album.set('foo', 'bar') - eq_(album._dump_save(), {'foo': 'bar'}) + album.set("foo", "bar") + eq_(album._dump_save(), {"foo": "bar"}) -def test_extend(): - ok_(Object.extend('Album')) +def test_extend(): # type: () -> None + ok_(Object.extend("Album")) -def test_update_data(): +def test_update_data(): # type: ignore album = Album() - album._update_data({'title': 'Once', 'artist': 'nightwish'}) - eq_(album._attributes, {'title': 'Once', 'artist': 'nightwish'}) + album._update_data({"title": "Once", "artist": "nightwish"}) + eq_(album._attributes, {"title": "Once", "artist": "nightwish"}) -def test_dump(): +def test_dump(): # type: () -> None album = Album() - album.set('foo', 'bar') - eq_(album.dump(), {'foo': 'bar'}) + album.set("foo", "bar") + eq_(album.dump(), {"foo": "bar"}) -def test_to_pointer(): +def test_to_pointer(): # type: ignore album = Album() - album.set('foo', 'bar') + album.set("foo", "bar") album._to_pointer() @with_setup(setup_func) -def test_fetch(): - album = Album(title='Once') - band = Band(name='Nightwish') - album.set('parent', band) +def test_fetch(): # type: () -> None + album = Album(title="Once") + band = Band(name="Nightwish") + album.set("parent", band) album.save() - query = leancloud.Query(Album) - album = query.get(album.id) - assert album.get('parent').get('name') is None - - album.get('parent').fetch() - assert album.get('parent').get('name') == 'Nightwish' + album_1 = Album.create_without_data(album.id) + assert album_1.is_existed() is False + album_1.fetch(include=["parent"], select=["name", "parent"]) + assert album_1.is_existed() is True + assert album_1.get("parent").get("name") == "Nightwish" + assert not album_1.has("title") album.destroy() band.destroy() -def test_has(): +def test_has(): # type: () -> None album = Album() - album.set('foo', 'bar') - assert album.has('foo') is True - assert album.has('bar') is False + album.set("foo", "bar") + assert album.has("foo") is True + assert album.has("bar") is False -def test_get_set_acl(): +def test_existence(): # type: () -> None + album = Album() + assert album.is_existed() is False + + +def test_get_set_acl(): # type: () -> None acl = leancloud.ACL() album = Album() album.set_acl(acl) assert album.get_acl() == acl -def test_invalid_acl(): +def test_invalid_acl(): # type: () -> None album = Album() - assert_raises(TypeError, album.set, 'ACL', 1) + assert_raises(TypeError, album.set, "ACL", 1) assert_raises(TypeError, album.set_acl, 1) @with_setup(setup_func) -def test_relation(): +def test_relation(): # type: () -> None album = Album() band = Band() band.save() - album.relation('band') - relation = album.relation('band') + album.relation("band") + relation = album.relation("band") relation.add(band) album.save() @with_setup(setup_func) -def test_pointer(): - user = leancloud.User.create_without_data('555ed132e4b032867865884e') - score = leancloud.Object.extend('score') +def test_pointer(): # type: () -> None + user = leancloud.User.create_without_data("555ed132e4b032867865884e") + score = leancloud.Object.extend("score") s = score() - s.set('user', user) + s.set("user", user) s.save() @with_setup(setup_func) -def test_save_and_destroy_all(): - ObjToDelete = Object.extend('ObjToDelete') +def test_save_and_destroy_all(): # type: () -> None + ObjToDelete = Object.extend("ObjToDelete") objs = [ObjToDelete() for _ in range(3)] + already_saved_obj = ObjToDelete() + already_saved_obj.save() + objs.append(already_saved_obj) Object.save_all(objs) assert all(not x.is_new() for x in objs) @@ -280,52 +309,53 @@ def test_save_and_destroy_all(): @with_setup(setup_func) -def test_fetch_when_save(): - Foo = Object.extend('Foo') +def test_fetch_when_save(): # type: () -> None + Foo = Object.extend("Foo") foo = Foo() - foo.fetch_when_save = True - foo.set('counter', 1) + foo.set("counter", 1) foo.save() - assert foo.get('counter') == 1 + assert foo.created_at == foo.updated_at + assert foo.get("counter") == 1 foo_from_other_thread = leancloud.Query(Foo).get(foo.id) - assert foo_from_other_thread.get('counter') == 1 - foo_from_other_thread.set('counter', 100) + assert foo_from_other_thread.get("counter") == 1 + foo_from_other_thread.set("counter", 100) foo_from_other_thread.save() - foo.increment('counter', 3) - foo.save() - eq_(foo.get('counter'), 103) + foo.increment("counter", 3) + foo.save(fetch_when_save=True) + eq_(foo.get("counter"), 103) foo.destroy() @with_setup(setup_func) -def test_save_with_where(): - Foo = Object.extend('Foo') +def test_save_with_where(): # type: () -> None + Foo = Object.extend("Foo") foo = Foo(aNumber=1) - assert_raises(TypeError, foo.save, where=Foo.query) + assert_raises(TypeError, foo.save, where=Foo.query) # type: ignore - assert_raises(TypeError, foo.save, where=leancloud.Query('SomeClassNotEqualToFoo')) + assert_raises(TypeError, foo.save, where=leancloud.Query("SomeClassNotEqualToFoo")) foo.save() - foo.set('aNumber', 2) + foo.set("aNumber", 2) try: - foo.save(where=leancloud.Query('Foo').equal_to('aNumber', 2)) + foo.save(where=leancloud.Query("Foo").equal_to("aNumber", 2)) except leancloud.LeanCloudError as e: assert e.code == 305 - foo.save(where=leancloud.Query('Foo').equal_to('aNumber', 1)) - assert leancloud.Query('Foo').get(foo.id).get('aNumber') == 2 + foo.save(where=leancloud.Query("Foo").equal_to("aNumber", 1)) + assert leancloud.Query("Foo").get(foo.id).get("aNumber") == 2 + @with_setup(setup_func) -def test_modify_class_name(): +def test_modify_class_name(): # type: () -> None class Philosopher(Object): - class_name = 'Teacher' + class_name = "Teacher" - @Object.as_class('Student') + @Object.as_class("Student") class Physicist(Object): pass @@ -334,8 +364,37 @@ class Physicist(Object): aristotle = Physicist() aristotle.save() - assert Query('Teacher').get(plato.id) - assert Query('Student').get(aristotle.id) + assert Query("Teacher").get(plato.id) + assert Query("Student").get(aristotle.id) plato.destroy() aristotle.destroy() + + +@with_setup(setup_func) +def test_create_without_data(): # type: () -> None + Foo = Object.extend("Foo") + foo1 = Foo(aNumber=2) + foo1.save() + foo2 = Foo.create_without_data(foo1.id) + foo2.set("aNumber", 3) + foo2.save() + assert foo1.id == foo2.id + assert Foo.query.get(foo1.id).get("aNumber") == 3 + foo1.destroy() + + +@with_setup(setup_func) +def test_time_zone(): + TestTimeZone = Object.extend("TestTimeZone") + now = datetime.now() + obj = TestTimeZone() + obj.set("date", now) + obj.save() + + obj = TestTimeZone.query.get(obj.id) + assert obj.created_at.tzinfo == tz.tzlocal() + assert obj.updated_at.tzinfo == tz.tzlocal() + assert obj.get("date").tzinfo == tz.tzlocal() + + obj.destroy() diff --git a/tests/test_op.py b/tests/test_op.py index 2eb53fdd..fb0168ab 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -4,24 +4,25 @@ from __future__ import division from __future__ import print_function +from nose.tools import assert_equal + import leancloud -from leancloud import Object -from leancloud import operation +from leancloud import operation # type:ignore -__author__ = 'asaka ' +__author__ = "asaka " -def test_set(): +def test_set(): # type: () -> None s = operation.Set(10) assert s.value == 10 -def test_unset(): +def test_unset(): # type: () -> None s = operation.Unset() assert s._apply(operation.Set(10)) == operation._UNSET -def test_increment(): +def test_increment(): # type: () -> None s = operation.Increment(1) assert s.amount == 1 previous = operation.Increment(2) @@ -29,20 +30,15 @@ def test_increment(): assert isinstance(new, operation.Increment) assert new.amount == 3 -def test_apply_relation_op(): - album = leancloud.Object.create("Album", - objectId="abc001", - title="variety") - band1 = leancloud.Object.create("Band", - objectId="abc101", - name="xxx") - band2 = leancloud.Object.create("Band", - objectId="abc102", - name="ooo") + +def test_apply_relation_op(): # type: () -> None + album = leancloud.Object.create("Album", objectId="abc001", title="variety") + band1 = leancloud.Object.create("Band", objectId="abc101", name="xxx") + band2 = leancloud.Object.create("Band", objectId="abc102", name="ooo") relation = album.relation("band") - op = operation.Relation([band1], [band2]) + op = operation.Relation([band1], [band2]) val = op._apply(None, album, "band") assert isinstance(val, leancloud.Relation) val._ensure_parent_and_key(album, "band") @@ -50,3 +46,16 @@ def test_apply_relation_op(): val = op._apply(relation) assert isinstance(val, leancloud.Relation) val._ensure_parent_and_key(album, "band") + + +def test_bit_op(): # type: () -> None + unset = operation.Unset() + + add_ = operation.BitAnd(123) + assert_equal(add_._merge(unset).value, 0) + + or_ = operation.BitOr(321) + assert_equal(or_._merge(unset).value, 321) + + xor = operation.BitOr(321) + assert_equal(xor._merge(unset).value, 321) diff --git a/tests/test_push.py b/tests/test_push.py index 71e22897..056cce4c 100644 --- a/tests/test_push.py +++ b/tests/test_push.py @@ -5,29 +5,27 @@ from __future__ import print_function import os -from datetime import datetime +import time +from datetime import datetime, timedelta -from nose.tools import with_setup +from nose.tools import with_setup # type: ignore import leancloud from leancloud import push -__author__ = 'asaka' +__author__ = "asaka" def setup_func(): - leancloud.init( - os.environ['APP_ID'], - os.environ['APP_KEY'] - ) + leancloud.init(os.environ["APP_ID"], os.environ["APP_KEY"]) @with_setup(setup_func) -def test_basic_push(): - instanlation = leancloud.Installation() - instanlation.set('deviceType', 'ios') - instanlation.set('deviceToken', 'xxx') - instanlation.save() +def test_basic_push(): # type: () -> None + installation = leancloud.Installation() + installation.set("deviceType", "ios") + installation.set("deviceToken", "xxx") + installation.save() data = { "alert": { @@ -40,7 +38,30 @@ def test_basic_push(): "launch-image": "", } } - t = datetime.fromtimestamp(0) - query = leancloud.Query('_Installation').equal_to('objectId', 'xxx') - notification = push.send(data, where=query, push_time=datetime.now()) - assert(notification.id) + query = leancloud.Query("_Installation").equal_to("deviceToken", "xxx") + now = datetime.now() + two_hours_later = now + timedelta(hours=2) + try: + notification = push.send( + data, + where=query, + push_time=now, + expiration_time=two_hours_later, + prod="dev", + flow_control=0, + ) + except leancloud.LeanCloudError as e: + # LeanCloudError: [1] The iOS certificate file is expired or disabled. + assert e.code == 1 + else: + # flow_control = 0 <=> flow_control = 1000 by rest api design + time.sleep(5) # notification write may have delay + notification.fetch() + assert notification.id + # Test that notification is read only. + try: + notification.save() + except leancloud.LeanCloudError as e: + assert e.code == 1 + else: + raise Exception() diff --git a/tests/test_query.py b/tests/test_query.py index 52cb1c78..092d65f7 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -9,6 +9,7 @@ from datetime import datetime from nose.tools import eq_ +from nose.tools import assert_equal from nose.tools import with_setup from nose.tools import raises @@ -16,16 +17,18 @@ from leancloud import Query from leancloud import Object from leancloud import GeoPoint +from leancloud.file_ import File -__author__ = 'asaka ' +__author__ = "asaka " class GameScore(Object): pass -number = Object.extend('number') -A = Object.extend('A') -B = Object.extend('B') + +number = Object.extend("number") +A = Object.extend("A") +B = Object.extend("B") # unmark the test to clean up the test data @@ -41,31 +44,37 @@ class GameScore(Object): # k.destroy() -def setup_func(): - leancloud.client.USE_MASTER_KEY = None - leancloud.client.APP_ID = None - leancloud.client.APP_KEY = None - leancloud.client.MASTER_KEY = None - leancloud.init( - os.environ['APP_ID'], - os.environ['APP_KEY'] - ) - - olds = Query(GameScore).find() - # make sure the test data does not change, else re-initialize it - if len(olds) == 10: - pass - else: - for old in olds: - old.destroy() - - for i in range(10): - game_score = GameScore() - game_score.set('score', i) - game_score.set('playerName', '张三') - game_score.set('location', GeoPoint(latitude=i, longitude=-i)) - game_score.set('random', random.randrange(100)) - game_score.save() +def make_setup_func(use_master_key=False): + def setup_func(): + leancloud.client.USE_MASTER_KEY = None + leancloud.client.APP_ID = None + leancloud.client.APP_KEY = None + leancloud.client.MASTER_KEY = None + if use_master_key: + leancloud.init(os.environ["APP_ID"], master_key=os.environ["MASTER_KEY"]) + else: + leancloud.init(os.environ["APP_ID"], os.environ["APP_KEY"]) + + olds = Query(GameScore).find() + # make sure the test data does not change, else re-initialize it + if len(olds) == 10: + pass + else: + for old in olds: + old.destroy() + + for i in range(10): + game_score = GameScore() + game_score.set("score", i) + game_score.set("playerName", "张三") + game_score.set("location", GeoPoint(latitude=i, longitude=-i)) + game_score.set("random", random.randrange(100)) + f = File("Blah.txt", open("tests/sample_text.txt", "rb")) + f.save() + game_score.set("attachment", f) + game_score.save() + + return setup_func def match_key_setup(): @@ -82,13 +91,13 @@ def match_key_setup(): for k in old2: k.destroy() - for i in range(5): + for _i in range(5): for k in range(10): a = A() - a.set('age', k) + a.set("age", k) a.save() b = B() - b.set('work_year', k) + b.set("work_year", k) b.save() @@ -100,323 +109,440 @@ def destroy_func(): @raises(ValueError) -def test_query_error(): - Query(123) +def test_query_error(): # type: () -> None + Query(123) # type: ignore -# @with_setup(setup_func, destroy_func) +# @with_setup(make_setup_func(), destroy_func) # def test_save(): # assert game_scores[0].id -@with_setup(setup_func, destroy_func) -def test_save_date(): - DateObject = Object.extend('DateObject') +@with_setup(make_setup_func(), destroy_func) +def test_save_date(): # type: () -> None + DateObject = Object.extend("DateObject") now = datetime.now() d = DateObject(date=now) d.save() - server_date = Query(DateObject).get(d.id).get('date') - assert now.isoformat().split('.')[0] == server_date.isoformat().split('.')[0] + server_date = Query(DateObject).get(d.id).get("date") + assert now.isoformat().split(".")[0] == server_date.isoformat().split(".")[0] -def test_batch(): - foo = Object.create('Foo') - bar = Object.create('Bar') - bar.set('baz', 'baz') - foo.set('bar', bar) +def test_batch(): # type: () -> None + foo = Object.create("Foo") + bar = Object.create("Bar") + bar.set("baz", "baz") + foo.set("bar", bar) bar.save() foo.save() # TODO: check if relation is corrected -@with_setup(setup_func, destroy_func) -def test_relation(): - foo = Object.extend('Foo')() - foo.set('a', 1) - bar = Object.extend('Bar')() - bar.set('baz', 'baz') +@with_setup(make_setup_func(), destroy_func) +def test_relation(): # type: () -> None + foo = Object.extend("Foo")() + foo.set("a", 1) + bar = Object.extend("Bar")() + bar.set("baz", "baz") bar.save() - relation = foo.relation('list') + relation = foo.relation("list") relation.add(bar) foo.save() -@with_setup(setup_func, destroy_func) -def test_basic_query(): +@with_setup(make_setup_func(), destroy_func) +def test_basic_query(): # type: () -> None # find - results = GameScore.query.find() + results = GameScore.query.find() # type:ignore eq_(len(results), 10) # first game_score = GameScore.query.first() assert game_score + assert game_score.get("objectId") + + assert isinstance(game_score.get("createdAt"), datetime) + assert isinstance(game_score.get("updatedAt"), datetime) # get GameScore.query.get(game_score.id) + # get nonexist + assert GameScore.query.get("NonexistentID").is_existed() is False + # count eq_(GameScore.query.count(), 10) # descending and add_descending - q = Query(GameScore).add_descending('score').descending('score') - eq_([x.get('score') for x in q.find()], list(range(9, -1, -1))) + q = Query(GameScore).add_descending("score").descending("score") + eq_([x.get("score") for x in q.find()], list(range(9, -1, -1))) # greater_than - q = Query(GameScore).greater_than('score', 5).ascending('score') - eq_([x.get('score') for x in q.find()], list(range(6, 10))) + q = Query(GameScore).greater_than("score", 5).ascending("score") + eq_([x.get("score") for x in q.find()], list(range(6, 10))) - q = Query(GameScore).greater_than_or_equal_to('score', 5).ascending('score') - eq_([x.get('score') for x in q.find()], list(range(5, 10))) + q = Query(GameScore).greater_than_or_equal_to("score", 5).ascending("score") + eq_([x.get("score") for x in q.find()], list(range(5, 10))) - q = Query(GameScore).less_than('score', 5).ascending('score') - eq_([x.get('score') for x in q.find()], list(range(0, 5))) + q = Query(GameScore).less_than("score", 5).ascending("score") + eq_([x.get("score") for x in q.find()], list(range(0, 5))) - q = Query(GameScore).less_than_or_equal_to('score', 5).ascending('score') - eq_([x.get('score') for x in q.find()], list(range(0, 6))) + q = Query(GameScore).less_than_or_equal_to("score", 5).ascending("score") + eq_([x.get("score") for x in q.find()], list(range(0, 6))) - q = Query(GameScore).contained_in('score', [1, 2, 3]).ascending('score') - eq_([x.get('score') for x in q.find()], list(range(1, 4))) + q = Query(GameScore).contained_in("score", [1, 2, 3]).ascending("score") + eq_([x.get("score") for x in q.find()], list(range(1, 4))) - q = Query(GameScore).not_contained_in('score', [0, 1, 2, 3]).ascending('score') - eq_([x.get('score') for x in q.find()], list(range(4, 10))) + q = Query(GameScore).not_contained_in("score", [0, 1, 2, 3]).ascending("score") + eq_([x.get("score") for x in q.find()], list(range(4, 10))) - q = Query(GameScore).select('score') - assert not q.find()[0].has('playerName') + q = Query(GameScore).select(["score"]) + assert not q.find()[0].has("playerName") -def test_or_and_query(): - q1 = Query(GameScore).greater_than('score', 5) - q2 = Query(GameScore).less_than('score', 10) - q3 = Query(GameScore).equal_to('playerName', 'foobar') +def test_or_and_query(): # type: () -> None + q1 = Query(GameScore).greater_than("score", 5) + q2 = Query(GameScore).less_than("score", 10) + q3 = Query(GameScore).equal_to("playerName", "foobar") q = Query.and_(q1, q2, q3) - assert q.dump() == {'where': {'$and': [{'score': {'$gt': 5}}, {'score': {'$lt': 10}}, {'playerName': 'foobar'}]}} + assert q.dump() == { + "where": { + "$and": [ + {"score": {"$gt": 5}}, + {"score": {"$lt": 10}}, + {"playerName": "foobar"}, + ] + } + } q = Query.or_(q1, q2, q3) - assert q.dump() == {'where': {'$or': [{'score': {'$gt': 5}}, {'score': {'$lt': 10}}, {'playerName': 'foobar'}]}} - - -@with_setup(setup_func) -def test_multiple_order(): - MultipleOrderObject = leancloud.Object.extend('MultipleOrderObject') + assert q.dump() == { + "where": { + "$or": [ + {"score": {"$gt": 5}}, + {"score": {"$lt": 10}}, + {"playerName": "foobar"}, + ] + } + } + + +@with_setup(make_setup_func()) +def test_query_acl(): # type: () -> None + TestACLObject = leancloud.Object.extend("TestACLObject") + o = TestACLObject(content="xxx") + acl = leancloud.ACL() + acl.set_public_read_access(True) + acl.set_public_write_access(True) + acl.set_write_access("xxxxx", True) + o.set_acl(acl) + o.save() + acl_data = o.get_acl().dump() + o = TestACLObject.query.include_acl().equal_to("objectId", o.id).first() + assert_equal(acl_data, o.get_acl().dump()) + o.destroy() + + +@with_setup(make_setup_func()) +def test_multiple_order(): # type: () -> None + MultipleOrderObject = leancloud.Object.extend("MultipleOrderObject") for obj in Query(MultipleOrderObject).find(): obj.destroy() MultipleOrderObject(a=1, b=10).save() MultipleOrderObject(a=10, b=20).save() MultipleOrderObject(a=1, b=3).save() q = Query(MultipleOrderObject) - q.add_descending('a') - q.add_descending('b') + q.add_descending("a") + q.add_descending("b") r = q.find() for i in range(1, len(r)): - assert r[i - 1].get('a') >= r[i].get('a') - assert r[i - 1].get('b') >= r[i].get('b') + assert r[i - 1].get("a") >= r[i].get("a") + assert r[i - 1].get("b") >= r[i].get("b") @raises(ValueError) -def test_or_erorr(): - Query(GameScore).or_('score') +def test_or_erorr(): # type: () -> None + Query(GameScore).or_("score") # type: ignore -@with_setup(setup_func) -def test_skip(): +@with_setup(make_setup_func()) +def test_skip(): # type: () -> None q = Query(GameScore) q.skip(5) assert q._skip == 5 -def test_limit(): +@with_setup(make_setup_func()) +def test_size_equal_to(): # type: () -> None + TestClass = leancloud.Object.extend("TestSizeEqualTo") + obj1 = TestClass(a=[1, 2, 3]) + obj1.save() + obj2 = TestClass(a=[1, 2, 3, 4, 5]) + obj2.save() + result = Query(TestClass).size_equal_to("a", 3).find() + assert len(result) == 1 + assert result[0].id == obj1.id + obj1.destroy() + obj2.destroy() + + +def test_limit(): # type: () -> None q = Query(GameScore) q.limit(121) assert q._limit == 121 @raises(ValueError) -def test_limit_error(): +def test_limit_error(): # type: () -> None Query(GameScore).limit(1001) -@with_setup(setup_func) -def test_cloud_query(): +@with_setup(make_setup_func()) +def test_cloud_query(): # type: () -> None q = Query(GameScore) - result = q.do_cloud_query('select count(*), * from GameScore where score<11', ['score']) -# results = result.results -# assert all(obj in game_scores for obj in results) -# assert all(obj in results for obj in game_scores) + result = q.do_cloud_query( + "select count(*), * from GameScore where score<11", ["score"] + ) assert result.count == 10 - assert result.class_name == 'GameScore' + assert result.class_name == "GameScore" -@with_setup(setup_func) -def test_or_(): - q1 = Query(GameScore).greater_than('score', -1) - q2 = Query(GameScore).equal_to('name', 'x') - result = Query.or_(q1, q2).ascending('score').find() - eq_([i.get('score') for i in result], list(range(10))) +@with_setup(make_setup_func()) +def test_or_(): # type: () -> None + q1 = Query(GameScore).greater_than("score", -1) + q2 = Query(GameScore).equal_to("name", "x") + result = Query.or_(q1, q2).ascending("score").find() + eq_([i.get("score") for i in result], list(range(10))) -@with_setup(setup_func) -def test_and_(): - q1 = Query(GameScore).greater_than('score', 2) - q2 = Query(GameScore).greater_than('score', 3) - result = Query.and_(q1, q2).ascending('score').find() - eq_([i.get('score') for i in result], list(range(4, 10))) +@with_setup(make_setup_func()) +def test_and_(): # type: () -> None + q1 = Query(GameScore).greater_than("score", 2) + q2 = Query(GameScore).greater_than("score", 3) + result = Query.and_(q1, q2).ascending("score").find() + eq_([i.get("score") for i in result], list(range(4, 10))) @raises(ValueError) -def test_and_error(): - Query(GameScore).and_('score') -# @with_setup(setup_func) +def test_and_error(): # type: () -> None + Query(GameScore).and_("score") # type: ignore + + +# @with_setup(make_setup_func()) # def test_dump(): # q = Query(GameScore) -@with_setup(setup_func) -def test_not_equal_to(): - result = Query(GameScore).not_equal_to('playerName', '李四').find() +@with_setup(make_setup_func()) +def test_not_equal_to(): # type: () -> None + result = Query(GameScore).not_equal_to("playerName", "李四").find() assert len(result) == 10 -@with_setup(setup_func) -def test_contains_all(): - q = Query(GameScore).contains_all('score', [5]) +@with_setup(make_setup_func()) +def test_contains_all(): # type: () -> None + q = Query(GameScore).contains_all("score", [5]) result = q.find() - eq_([i.get('score') for i in result], [5]) + eq_([i.get("score") for i in result], [5]) -@with_setup(setup_func) -def test_exist_and_does_not_exists(): - assert Query(GameScore).does_not_exists('oops').find() - result = Query(GameScore).exists('playerName').find() +@with_setup(make_setup_func()) +def test_exist_and_does_not_exists(): # type: () -> None + assert Query(GameScore).does_not_exist("oops").find() + result = Query(GameScore).exists("playerName").find() assert len(result) == 10 -@raises(TypeError) -def test_matched_error(): - Query(GameScore).matched('score', 1) +@with_setup(make_setup_func()) +def test_exist_and_does_not_exist(): + assert Query(GameScore).does_not_exist("oops").find() + result = Query(GameScore).exists("playerName").find() + assert len(result) == 10 -@with_setup(setup_func) -def test_matched(): - result = Query(GameScore).matched('playerName', '^张', ignore_case=True, multi_line=True).find() - assert len(result) == 10 +@raises(TypeError) +def test_matched_error(): # type: () -> None + Query(GameScore).matched("score", 1) # type: ignore -@with_setup(setup_func) -def test_does_not_match_query(): - q = Query(GameScore).greater_than('score', -1) - result = Query(GameScore).does_not_match_query('playerName', q).find() +@with_setup(make_setup_func()) +def test_matched(): # type: () -> None + result = ( + Query(GameScore) + .matched("playerName", "^张", ignore_case=True, multi_line=True) + .find() + ) + assert len(result) == 10 -@with_setup(setup_func, match_key_setup) -def test_matches_key_in_query(): - q1 = Query(A).equal_to('age', 1) - q2 = Query(B) - result = q2.matches_key_in_query('work_year', 'age', q1).find() - assert len(result) == 5 +@with_setup(make_setup_func()) +def test_does_not_match_query(): # type: () -> None + q = Query(GameScore).greater_than("score", -1) + result = Query(GameScore).does_not_match_query("playerName", q).find() + assert len(result) == 10 -@with_setup(setup_func, match_key_setup) -def test_matches_key_in_query(): - q1 = Query(A).equal_to('age', 1) +@with_setup(make_setup_func(), match_key_setup) +def test_matches_key_in_query(): # type: () -> None + q1 = Query(A).equal_to("age", 1) q2 = Query(B) - result = q2.matches_key_in_query('work_year', 'age', q1).find() + result = q2.matches_key_in_query("work_year", "age", q1).find() assert len(result) == 5 -@with_setup(setup_func, match_key_setup) -def test_does_not_match_key_in_query(): - q1 = Query(A).equal_to('age', 1) +@with_setup(make_setup_func(), match_key_setup) +def test_does_not_match_key_in_query(): # type: () -> None + q1 = Query(A).equal_to("age", 1) q2 = Query(B) - result = q2.does_not_match_key_in_query('work_year', 'age', q1).find() + result = q2.does_not_match_key_in_query("work_year", "age", q1).find() assert len(result) == 45 -@with_setup(setup_func) -def test_contains(): - q = Query(GameScore).contains('playerName', '三') +@with_setup(make_setup_func()) +def test_contains(): # type: () -> None + q = Query(GameScore).contains("playerName", "三") eq_(len(q.find()), 10) -@with_setup(setup_func) -def test_startswith(): - q = Query(GameScore).startswith('playerName', '张') +@with_setup(make_setup_func()) +def test_startswith(): # type: () -> None + q = Query(GameScore).startswith("playerName", "张") eq_(len(q.find()), 10) -@with_setup(setup_func) -def test_endswith(): - q = Query(GameScore).endswith('playerName', '三') +@with_setup(make_setup_func()) +def test_endswith(): # type: () -> None + q = Query(GameScore).endswith("playerName", "三") eq_(len(q.find()), 10) -@with_setup(setup_func) -def test_add_ascending(): - result = Query(GameScore).add_ascending('score').find() - eq_([i.get('score') for i in result], list(range(10))) +@with_setup(make_setup_func()) +def test_add_ascending(): # type: () -> None + result = Query(GameScore).add_ascending("score").find() + eq_([i.get("score") for i in result], list(range(10))) -@with_setup(setup_func) -def test_near(): - result = Query(GameScore).near('location', GeoPoint(latitude=0, longitude=0)).find() - eq_([i.get('score') for i in result], list(range(10))) +@with_setup(make_setup_func()) +def test_near(): # type: () -> None + result = Query(GameScore).near("location", GeoPoint(latitude=0, longitude=0)).find() + eq_([i.get("score") for i in result], list(range(10))) -@with_setup(setup_func) -def test_within_radians(): - result = Query(GameScore).within_radians('location', GeoPoint(latitude=0, longitude=0), 1).find() - eq_([i.get('score') for i in result], list(range(10))) +@with_setup(make_setup_func()) +def test_within_radians(): # type: () -> None + result = ( + Query(GameScore) + .within_radians("location", GeoPoint(latitude=0, longitude=0), 1) + .find() + ) + eq_([i.get("score") for i in result], list(range(10))) -@with_setup(setup_func) -def test_within_miles(): - result = Query(GameScore).within_miles('location', GeoPoint(latitude=0, longitude=0), 4000).find() - eq_([i.get('score') for i in result], list(range(10))) +@with_setup(make_setup_func()) +def test_within_miles(): # type: () -> None + result = ( + Query(GameScore) + .within_miles("location", GeoPoint(latitude=0, longitude=0), 4000) + .find() + ) + eq_([i.get("score") for i in result], list(range(10))) -@with_setup(setup_func) -def test_within_kilometers(): - result = Query(GameScore).within_kilometers('location', GeoPoint(latitude=0, longitude=0), 4000).find() +@with_setup(make_setup_func()) +def test_within_kilometers(): # type: () -> None + result = ( + Query(GameScore) + .within_kilometers("location", GeoPoint(latitude=0, longitude=0), 4000) + .find() + ) assert len(result) == 10 # to_check -@with_setup(setup_func) -def test_within_geobox(): - result = Query(GameScore).within_geo_box('location', (0, 0), (11, 11)).find() - eq_([i.get('score') for i in result], [0]) +@with_setup(make_setup_func()) +def test_within_geobox(): # type: () -> None + result = Query(GameScore).within_geo_box("location", (0, 0), (11, 11)).find() + eq_([i.get("score") for i in result], [0]) -@with_setup(setup_func) -def test_include(): - result = Query(GameScore).include(['score']).find() +@with_setup(make_setup_func()) +def test_include(): # type: () -> None + result = Query(GameScore).include(["score"]).find() assert len(result) == 10 +@with_setup(make_setup_func()) +def test_attachment(): # type: () -> None + result = Query(GameScore).first() + print(result.dump()) + attachment = result.get('attachment') + assert attachment.url -@with_setup(setup_func) -def test_select(): - result = Query(GameScore).select(['score', 'playerName']).find() +@with_setup(make_setup_func()) +def test_select(): # type: () -> None + result = Query(GameScore).select(["score", "playerName"]).find() eq_(len(result), 10) -@with_setup(setup_func) -def test_pointer_query(): - foo = Object.create('Foo') - bar = Object.create('Bar') +@with_setup(make_setup_func()) +def test_pointer_query(): # type: () -> None + foo = Object.create("Foo") + bar = Object.create("Bar") bar.save() - foo.set('bar', bar) + foo.set("bar", bar) foo.save() - q = Query('Foo').equal_to('bar', bar) + q = Query("Foo").equal_to("bar", bar) assert len(q.find()) == 1 - inner_query = leancloud.Query('Post') + inner_query = leancloud.Query("Post") inner_query.exists("image") - query = leancloud.Query('Comment') + query = leancloud.Query("Comment") query.matches_query("post", inner_query) - assert query.dump() == {'where': {'post': {'$inQuery': {'className': 'Post', 'where': {'image': {'$exists': True}}}}}} + assert query.dump() == { + "where": { + "post": { + "$inQuery": {"className": "Post", "where": {"image": {"$exists": True}}} + } + } + } + @raises(ValueError) -def test_near_not_none(): - Query('test').near('oops', None) +def test_near_not_none(): # type: () -> None + Query("test").near("oops", None) + + +@with_setup(make_setup_func()) +def test_query_include_acl(): + with_acl = Query(GameScore).include_acl(True).first() + assert with_acl.get_acl() is not None + explicit_without_acl = Query(GameScore).include_acl(False).first() + assert explicit_without_acl.get_acl() is None + implicit_without_acl = Query(GameScore).first() + assert implicit_without_acl.get_acl() is None + + +@with_setup(make_setup_func()) +def test_save_with_query(): + Account = leancloud.Object.extend("Account") + account = Account(balance=10) + account.save() + account.increment("balance", -100) + try: + account.save(where=Account.query.greater_than_or_equal_to("balance", 100)) + except leancloud.LeanCloudError as e: + assert e.code == 305 + else: + raise Exception("305 error not raised") + account.destroy() + + +@with_setup(make_setup_func(use_master_key=True)) +def test_scan(): + cursor = GameScore.query.scan(batch_size=2, scan_key="random") + r = 0 + for i in cursor: + assert r <= i.get("random") + r = i.get("random") diff --git a/tests/test_relation.py b/tests/test_relation.py index de31774b..2c06f141 100644 --- a/tests/test_relation.py +++ b/tests/test_relation.py @@ -12,14 +12,11 @@ from leancloud import Object from leancloud import Relation -__author__ = 'asaka ' +__author__ = "asaka " def setup_func(): - leancloud.init( - os.environ['APP_ID'], - os.environ['APP_KEY'] - ) + leancloud.init(os.environ["APP_ID"], os.environ["APP_KEY"]) class Band(Object): @@ -30,28 +27,28 @@ class Album(Object): pass -def test_create_relation(): +def test_create_relation(): # type: () -> None album = Album() - r = Relation(album, 'band') + r = Relation(album, "band") assert r @with_setup(setup_func) -def test_query_relation(): - album = Album(title='variety') - band1 = Band(name='xxx') +def test_query_relation(): # type: () -> None + album = Album(title="variety") + band1 = Band(name="xxx") band1.save() - band2 = Band(name='ooo') + band2 = Band(name="ooo") band2.save() - relation = album.relation('band') + relation = album.relation("band") relation.add(band1) relation.add(band2) album.save() - album = leancloud.Query('Album').get(album.id) - relation = album.relation('band') - bands = relation.query().find() + album = leancloud.Query("Album").get(album.id) + relation = album.relation("band") + bands = relation.query.find() assert band1.id in [x.id for x in bands] assert band2.id in [x.id for x in bands] diff --git a/tests/test_role.py b/tests/test_role.py index 4ca00fe7..fc09c1f0 100644 --- a/tests/test_role.py +++ b/tests/test_role.py @@ -6,13 +6,13 @@ import os -from nose.tools import eq_ -from nose.tools import with_setup +from nose.tools import eq_ # type: ignore +from nose.tools import with_setup # type: ignore import leancloud -__author__ = 'asaka ' +__author__ = "asaka " def setup_func(): @@ -20,33 +20,35 @@ def setup_func(): leancloud.client.APP_ID = None leancloud.client.APP_KEY = None leancloud.client.MASTER_KEY = None - leancloud.init( - os.environ['APP_ID'], - master_key=os.environ['MASTER_KEY'] - ) + leancloud.init(os.environ["APP_ID"], master_key=os.environ["MASTER_KEY"]) -def test_init(): + +def test_init(): # type: () -> None acl = leancloud.ACL() - role = leancloud.Role('xxx', acl) + role = leancloud.Role("xxx", acl) assert role - assert role.get_name() == 'xxx' + assert role.get_name() == "xxx" assert role.get_acl() == acl + assert role.users + assert role.roles -def test_init_with_default_acl(): - role = leancloud.Role('qux') +def test_init_with_default_acl(): # type: () -> None + role = leancloud.Role("qux") assert role - assert role.get_name() == 'qux' + assert role.get_name() == "qux" + role.name = "quux" + assert role.name == "quux" acl = role.get_acl() - assert acl.dump() == {'*': {'read': True}} + assert acl.dump() == {"*": {"read": True}} @with_setup(setup=setup_func) -def test_role_query(): +def test_role_query(): # type: () -> None roles = leancloud.Query(leancloud.Role).limit(1000).find() leancloud.Object.destroy_all(roles) - role = leancloud.Role('test_role') + role = leancloud.Role("test_role") role.save() eq_(leancloud.Query(leancloud.Role).count(), 1) diff --git a/tests/test_status.py b/tests/test_status.py new file mode 100644 index 00000000..00fca542 --- /dev/null +++ b/tests/test_status.py @@ -0,0 +1,84 @@ +# coding: utf-8 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import os +import time + +from nose.tools import assert_equal + +import leancloud +from leancloud import Status +from leancloud import InboxQuery + + +def setup(): + leancloud.init( + os.environ["APP_ID"], + app_key=os.environ["APP_KEY"], + master_key=os.environ["MASTER_KEY"], + ) + leancloud.use_master_key(True) + users = leancloud.Query(leancloud.User).find() + for u in users: + u.destroy() + leancloud.use_master_key(False) + + user1 = leancloud.User() + user1.set("username", "user1_name") + user1.set("password", "password") + user1.set_email("wow@leancloud.rocks") + user1.set_mobile_phone_number("18611111111") + user1.sign_up() + + +def test_send(): + status = Status(image="http://www.example.com", message="hello world!") + status.inbox_type = "privateMessage" + query = leancloud.Query("User").equal_to("username", "user1_name") + status.send(query) + assert status.id + assert status.created_at + status.destroy() + + +def test_send_to_followers(): + status = Status(image="http://www.example.com", message="hello world!") + status.send_to_followers() + assert status.id + assert status.created_at + status.destroy() + + +def test_send_private_status(): + status = Status(image="http://www.example.com", message="hello world!") + target = leancloud.User.create_without_data("foo") + status.send_private_status(target) + assert status.id + assert status.created_at + assert_equal(status.inbox_type, "private") + status.destroy() + + +def test_statuses_count(): + status = Status(image="http://www.example.com", message="hello world!") + status.send_private_status(leancloud.User.get_current()) + time.sleep(1) # wait server to sync + result = Status.count_unread_statuses(leancloud.User.get_current(), "private") + assert_equal(result.total, 1) + assert_equal(result.unread, 1) + status.destroy() + + +def test_inbox_query(): + status = Status(image="http://www.example.com", message="hello world!") + status.send_private_status(leancloud.User.get_current()) + time.sleep(1) # wait server to sync + saved = ( + InboxQuery().inbox_type("private").owner(leancloud.User.get_current()).first() + ) + assert_equal(saved.get("image"), status.get("image")) + assert_equal(saved.id, status.id) diff --git a/tests/test_sys_message.py b/tests/test_sys_message.py new file mode 100644 index 00000000..c52a490d --- /dev/null +++ b/tests/test_sys_message.py @@ -0,0 +1,48 @@ +# coding: utf-8 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +from datetime import datetime + +from nose.tools import assert_equal + +import leancloud +from leancloud import Conversation +from leancloud import SysMessage + + +def setup(): + leancloud.client.USE_MASTER_KEY = None + leancloud.client.APP_ID = None + leancloud.client.APP_KEY = None + leancloud.client.MASTER_KEY = None + leancloud.init(os.environ["APP_ID"], master_key=os.environ["MASTER_KEY"]) + + +def test_sys_message(): + conv = Conversation("testConversation", is_system=True) + conv.save() + msg = SysMessage() + msg.set("conv", conv) + msg.set("bin", False) + msg.set("msgId", "testmsgid") + msg.set("from", "testfromclient") + msg.set("fromIp", "0.0.0.0") + msg.set("data", '{"_lctext":"test!","_lctype":-1}') + msg.set("timestamp", 1503908409224) + msg.set("ackAt", 1503908409237) + msg.save() + + savedMsg = SysMessage.query.get(msg.id) + assert_equal(msg.conversation.id, savedMsg.conversation.id) + assert_equal(msg.message_id, savedMsg.message_id) + assert_equal(msg.from_client, savedMsg.from_client) + assert_equal(msg.from_ip, savedMsg.from_ip) + assert_equal(msg.data, savedMsg.data) + assert_equal(type(savedMsg.message_created_at), datetime) + assert_equal(type(savedMsg.ack_at), datetime) + + msg.destroy() diff --git a/tests/test_user.py b/tests/test_user.py index 0bb6622c..860618c3 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -5,18 +5,18 @@ from __future__ import print_function import os +import io import random -from nose.tools import with_setup +from nose.tools import with_setup # type: ignore import leancloud from leancloud import User from leancloud import Query from leancloud import File from leancloud.errors import LeanCloudError -from leancloud._compat import buffer_type -__author__ = 'asaka ' +__author__ = "asaka " def only_init(): @@ -24,10 +24,7 @@ def only_init(): leancloud.client.APP_ID = None leancloud.client.APP_KEY = None leancloud.client.MASTER_KEY = None - leancloud.init( - os.environ['APP_ID'], - master_key=os.environ['MASTER_KEY'] - ) + leancloud.init(os.environ["APP_ID"], master_key=os.environ["MASTER_KEY"]) def get_setup_func(use_master_key=True): @@ -37,28 +34,29 @@ def setup_func(): leancloud.client.APP_KEY = None leancloud.client.MASTER_KEY = None leancloud.init( - os.environ['APP_ID'], - app_key=os.environ['APP_KEY'], - master_key=os.environ['MASTER_KEY'] + os.environ["APP_ID"], + app_key=os.environ["APP_KEY"], + master_key=os.environ["MASTER_KEY"], ) users = Query(User).find() for u in users: u.destroy() user1 = User() - user1.set('username', 'user1_name') - user1.set('password', 'password') - user1.set_email('wow@leancloud.rocks') - user1.set_mobile_phone_number('18611111111') + user1.set("username", "user1_name") + user1.set("password", "password") + user1.set_email("wow@leancloud.rocks") + user1.set_mobile_phone_number("18611111111") user1.sign_up() user1.logout() user2 = User() - user2.set('username', 'user2_name') - user2.set('password', 'password') + user2.set("username", "user2_name") + user2.set("password", "password") user2.sign_up() user2.logout() leancloud.client.use_master_key(use_master_key) + return setup_func @@ -67,227 +65,308 @@ def destroy_func(): @with_setup(only_init) -def test_sign_up(): +def test_sign_up(): # type: () -> None user = User() - user.set('username', 'foo') - user.set('password', 'bar') + user.set("username", "foo") + user.set("password", "bar") user.sign_up() assert user._session_token @with_setup(only_init) -def test_sign_out(): +def test_sign_out(): # type: () -> None user = User() - user.sign_up('Musen', 'password') + user.sign_up("Musen", "password") user.logout() assert not user.is_current @with_setup(get_setup_func(), destroy_func) -def test_login(): +def test_login(): # type: () -> None user = User() - user.set('username', 'user1_name') - user.set('password', 'password') + user.set("username", "user1_name") + user.set("password", "password") user.login() user = User() - user.login('user1_name', 'password') + user.login("user1_name", "password") + + +@with_setup(get_setup_func(), destroy_func) +def test_login_with_email(): # type: () -> None + + user = User() + user.login(email="wow@leancloud.rocks", password="password") @with_setup(get_setup_func(), destroy_func) -def test_file_field(): +def test_file_field(): # type: () -> None user = User() - user.login('user1_name', 'password') - user.set('xxxxx', File('xxx.txt', buffer_type(b'qqqqq'))) + user.login("user1_name", "password") + user.set("xxxxx", File("xxx.txt", io.BytesIO(b"qqqqq"))) user.save() q = Query(User) saved_user = q.get(user.id) - assert isinstance(saved_user.get('xxxxx'), File) - assert saved_user.get('xxxxx').name == 'xxx.txt' + assert isinstance(saved_user.get("xxxxx"), File) + assert saved_user.get("xxxxx").name == "xxx.txt" @with_setup(get_setup_func()) -def test_follow(): +def test_follow(): # type: () -> None user1 = User() - user1.set('username', 'user1_name') - user1.set('password', 'password') + user1.set("username", "user1_name") + user1.set("password", "password") user1.login() user2 = User() - user2.set('username', 'user2_name') - user2.set('password', 'password') + user2.set("username", "user2_name") + user2.set("password", "password") user2.login() user1.follow(user2.id) @with_setup(get_setup_func()) -def test_follower_query(): +def test_follower_query(): # type: () -> None user1 = User() - user1.login('user1_name', 'password') + user1.login("user1_name", "password") user2 = User() - user2.login('user2_name', 'password') + user2.login("user2_name", "password") user2.follow(user1.id) query = User.create_follower_query(user1.id) assert query.first().id == user2.id -def test_followee_query(): - query = User.create_followee_query('1') - assert query._friendship_tag == 'followee' +def test_followee_query(): # type: () -> None + query = User.create_followee_query("1") + assert query._friendship_tag == "followee" assert query.dump() == { - 'where': { - 'user': { - '__type': 'Pointer', - 'className': '_User', - 'objectId': '1', - }, + "where": { + "user": {"__type": "Pointer", "className": "_User", "objectId": "1"}, }, } @with_setup(get_setup_func()) -def test_current_user(): +def test_current_user(): # type: () -> None user = User() - user.login('user1_name', 'password') + user.login("user1_name", "password") assert user.is_current assert User.get_current().id == user.id @with_setup(get_setup_func(use_master_key=False)) -def test_update_user(): +def test_update_user(): # type: () -> None user = User() - user.login('user1_name', 'password') - user.set('nickname', 'test_name') + user.login("user1_name", "password") + user.set("nickname", "test_name") user.save() @with_setup(get_setup_func()) -def test_user_become(): +def test_user_become(): # type: () -> None existed_user = User() - existed_user.login('user1_name', 'password') - session_token = existed_user.get_session_token() + existed_user.login("user1_name", "password") + session_token = existed_user.session_token user = User.become(session_token) - assert user.get('username') == existed_user.get('username') + assert user.get("username") == existed_user.get("username") @with_setup(get_setup_func()) -def test_login_with(): - data = { - 'uid': '1', - 'access_token': 'xxx' - } - User.login_with('xxx', data) +def test_login_with(): # type: () -> None + data = {"uid": "1", "access_token": "xxx"} + User.login_with("xxx", data) @with_setup(get_setup_func()) -def test_unlink_from(): - data = { - 'uid': '1', - 'access_token': 'xxx' - } - user = User.login_with('xxx', data) - user.unlink_from('xxx') +def test_unlink_from(): # type: () -> None + data = {"uid": "1", "access_token": "xxx"} + user = User.login_with("xxx", data) + user.unlink_from("xxx") @with_setup(get_setup_func()) -def test_is_linked(): - data = { - 'uid': '1', - 'access_token': 'xxx' - } - user = User.login_with('xxx', data) - assert user.is_linked('xxx') +def test_is_linked(): # type: () -> None + data = {"uid": "1", "access_token": "xxx"} + user = User.login_with("xxx", data) + assert user.is_linked("xxx") @with_setup(get_setup_func()) -def test_signup_or_login_with_mobile_phone(): +def test_signup_or_login_with_mobile_phone(): # type: () -> None try: - User.signup_or_login_with_mobile_phone('18611111111', '111111') + User.signup_or_login_with_mobile_phone("18611111111", "111111") except LeanCloudError as e: assert e.code == 603 @with_setup(get_setup_func()) -def test_update_password(): +def test_update_password(): # type: () -> None user = User() - user.login('user1_name', 'password') - user.update_password('password', 'new_password') - user.login('user1_name', 'new_password') + user.login("user1_name", "password") + user.update_password("password", "new_password") + user.login("user1_name", "new_password") @with_setup(get_setup_func()) -def test_get_methods(): +def test_get_methods(): # type: () -> None user = User() - user.login('user1_name', 'password') + user.login("user1_name", "password") + + user.set_username("new_user1") + assert user.get_username() == "new_user1" + + user.set_mobile_phone_number("18611111111x") + assert user.get_mobile_phone_number() == "18611111111x" + + user.set_password("new_password") + assert user._attributes.get("password") == "new_password" - user.set_username('new_user1') - assert user.get_username() == 'new_user1' + user.set_email("wow1@leancloud.rocks") + assert user.get_email() == "wow1@leancloud.rocks" - user.set_mobile_phone_number('18611111111x') - assert user.get_mobile_phone_number() == '18611111111x' - user.set_password('new_password') - assert user._attributes.get('password') == 'new_password' +@with_setup(get_setup_func()) +def test_get_roles(): # type: () -> None + role = leancloud.Role("xxx") + role.save() + + user = User() + user.login("user1_name", "password") + role.get_users().add(user) + role.save() + bind_roles = user.get_roles() + assert len(bind_roles) == 1 + assert bind_roles[0].get("name") == "xxx" - user.set_email('wow1@leancloud.rocks') - assert user.get_email() == 'wow1@leancloud.rocks' + role.destroy() @with_setup(get_setup_func()) -def test_request_password_reset(): +def test_request_password_reset(): # type: () -> None try: - User.request_password_reset('wow@leancloud.rocks') + User.request_password_reset("wow@leancloud.rocks") except LeanCloudError as e: - print(e.code) - assert u'请不要往同一个邮件地址发送太多邮件。' in e.error \ - or 'Too many emails sent to the same email address' in str(e) + assert u"请不要往同一个邮件地址发送太多邮件。" in e.error or "Too many emails" in str(e) @with_setup(get_setup_func()) -def test_request_email_verify(): +def test_request_email_verify(): # type: () -> None try: - User.request_email_verify('wow@leancloud.rocks') + User.request_email_verify("wow@leancloud.rocks") except LeanCloudError as e: - print(e) - assert '邮件验证功能' in str(e) \ - or '请不要往同一个邮件地址发送太多邮件' in str(e) \ - or 'Too many emails sent to the same email address' in str(e)\ - or 'Please enable verifying user email option in application settings' in str(e) + assert ( + "邮件验证功能" in str(e) + or "请不要往同一个邮件地址发送太多邮件" in str(e) + or "Too many emails" in str(e) + or "Please enable the option to verify user emails in application settings." + in str(e) + ) @with_setup(get_setup_func()) -def test_request_mobile_phone_verify(): +def test_request_mobile_phone_verify(): # type: () -> None try: - User.request_mobile_phone_verify('1861111' + str(random.randrange(1000, 9999))) + User.request_mobile_phone_verify("1861111" + str(random.randrange(1000, 9999))) except LeanCloudError as e: if e.code not in (213, 601): raise e +@with_setup(only_init) +def test_request_change_phone_number(): # type: () -> None + user1 = User() + user1.set("username", "py_test_change_phone") + user1.set("password", "password") + user1.sign_up() + try: + # phone number is from http://www.z-sms.com + User.request_change_phone_number("+8617180655340") + except LeanCloudError as e: + if e.code in (119, 213, 601, 605): + pass + elif "SMS sending exceeds limit" in e.error: + pass + elif "send too frequently" in e.error: + pass + else: + raise e + finally: + user1.logout() + + +@with_setup(only_init) +def test_change_phone_number(): # type: () -> None + try: + # phone number is from http://www.z-sms.com + User.change_phone_number("196784", "+8617180655340") + except LeanCloudError as e: + if e.code != 603: + raise e + else: + user1 = User() + user1.set("username", "py_test_change_phone") + user1.set("password", "password") + user1.login() + assert user1.get_mobile_phone_number() == "+8617180655340" + user1.destroy() + + @with_setup(get_setup_func()) -def test_request_password_reset_by_sms_code(): +def test_request_password_reset_by_sms_code(): # type: () -> None try: - User.request_password_reset_by_sms_code('1861111' + str(random.randrange(1000, 9999))) + User.request_password_reset_by_sms_code( + "1861111" + str(random.randrange(1000, 9999)) + ) except LeanCloudError as e: if e.code not in (213, 601): raise e @with_setup(get_setup_func()) -def test_reset_password_by_sms_code(): +def test_reset_password_by_sms_code(): # type: () -> None try: - User.reset_password_by_sms_code('1861111' + str(random.randrange(1000, 9999)), "password") + User.reset_password_by_sms_code( + str(random.randrange(100000, 999999)), + "password", + "1861111" + str(random.randrange(1000, 9999)) + ) except LeanCloudError as e: - if e.code != 1: + if e.code != 603: raise e - + @with_setup(get_setup_func()) -def test_request_login_sms_code(): +def test_request_login_sms_code(): # type: () -> None try: - User.request_login_sms_code('18611111111') + User.request_login_sms_code("18611111111") except LeanCloudError as e: if e.code not in (1, 215, 601): raise e + + +@with_setup(get_setup_func()) +def test_refresh_session_token(): + user = User() + user.set("username", "user1_name") + user.set("password", "password") + user.login() + old_session_token = user.get_session_token() + user.refresh_session_token() + assert old_session_token != user.get_session_token() + + +@with_setup(get_setup_func(use_master_key=False)) +def test_is_authenticated(): + user = User() + assert not user.is_authenticated() + + user._session_token = "invalid-session-token" + assert not user.is_authenticated() + + user = User() + user.set("username", "user1_name") + user.set("password", "password") + user.login() + assert user.is_authenticated() diff --git a/tests/test_util.py b/tests/test_util.py index a58c1d2a..e0973034 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -4,76 +4,60 @@ from __future__ import division from __future__ import print_function +import time + from nose.tools import eq_ -from leancloud import Object from leancloud import ACL from leancloud import GeoPoint +from leancloud import Object from leancloud import utils -__author__ = 'asaka ' +__author__ = "asaka " -def test_encode(): - Foo = Object.extend('Foo') +def test_encode(): # type: () -> None + Foo = Object.extend("Foo") obj = Foo() assert utils.encode(obj) == { - 'className': 'Foo', - '__type': 'Pointer', - 'objectId': None, + "className": "Foo", + "__type": "Pointer", + "objectId": None, } acl = ACL() assert utils.encode(acl) == {} - acl.set_read_access('xxx', True) - assert utils.encode(acl) == {'xxx': {'read': True}} + acl.set_read_access("xxx", True) + assert utils.encode(acl) == {"xxx": {"read": True}} point = GeoPoint() assert utils.encode(point) == { - '__type': 'GeoPoint', - 'longitude': 0, - 'latitude': 0, + "__type": "GeoPoint", + "longitude": 0, + "latitude": 0, } assert utils.encode([obj, acl, point]) == [ - { - 'className': 'Foo', - '__type': 'Pointer', - 'objectId': None, - }, { - 'xxx': {'read': True} - }, { - '__type': 'GeoPoint', - 'longitude': 0, - 'latitude': 0, - } + {"className": "Foo", "__type": "Pointer", "objectId": None}, + {"xxx": {"read": True}}, + {"__type": "GeoPoint", "longitude": 0, "latitude": 0}, ] - assert utils.encode({'a': obj, 'b': acl}) == { - 'a': { - 'className': 'Foo', - '__type': 'Pointer', - 'objectId': None, - }, - 'b': { - 'xxx': {'read': True} - }, + assert utils.encode({"a": obj, "b": acl}) == { + "a": {"className": "Foo", "__type": "Pointer", "objectId": None}, + "b": {"xxx": {"read": True}}, } -def test_decode(): - p = utils.decode('test_key', { - '__type': 'GeoPoint', - 'longitude': 0, - 'latitude': 0, - }) +def test_decode(): # type: () -> None + p = utils.decode("test_key", {"__type": "GeoPoint", "longitude": 0, "latitude": 0}) assert isinstance(p, GeoPoint) assert p.latitude == 0 assert p.longitude == 0 -def test_util(): - obj = Object.extend('Foo')() +def test_util(): # type: () -> None + obj = Object.extend("Foo")() def callback(o): callback.count += 1 @@ -89,6 +73,17 @@ def callback(o): assert callback.count == 2 -def test_sign_disable_hook(): - sign = utils.sign_disable_hook('__before_for_TestClass', 'test-master-key', '1453711871302') - eq_(sign, '1453711871302,f10c9dd65da84b564f1b9a8b57df4a07774bc77b') +def test_throttle(): # type: () -> None + env = {"life": 0} + + @utils.throttle(seconds=1) + def plus_one_second(): + env["life"] += 1 + + plus_one_second() + plus_one_second() + plus_one_second() + eq_(env["life"], 1) + time.sleep(2) + plus_one_second() + eq_(env["life"], 2)