diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 8ab34bd5a..a3d483f40 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -9,12 +9,18 @@ on: - '**' jobs: - black: - name: Black + check-formatting: + name: Check code/doc formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Run Black uses: lgeiger/black-action@master with: - args: --check src/ tests/ + args: --check src/ tests/ benchmarks/ + + - name: Install rST dependcies + run: python -m pip install doc8 + - name: Lint documentation for errors + run: python -m doc8 docs --max-line-length 88 --ignore-path-errors "docs/migrating.rst;D001" + # it has a table which is longer than 88 characters long diff --git a/.gitignore b/.gitignore index c3742608c..67d07686a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ output .DS_Store .tox .venv +.asv +venv diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..7a0b3b776 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,17 @@ +version: 2 +formats: [] + +build: + image: latest + +sphinx: + configuration: docs/conf.py + fail_on_warning: false + +python: + version: 3.7 + install: + - method: pip + path: . + extra_requirements: + - dev \ No newline at end of file diff --git a/README.md b/README.md index 8d86eda26..c70aca7ee 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,20 @@ message Greeting { } ``` -You can run the following: +You can run the following to invoke protoc directly: ```sh mkdir lib protoc -I . --python_betterproto_out=lib example.proto ``` +or run the following to invoke protoc via grpcio-tools: + +```sh +pip install grpcio-tools +python -m grpc_tools.protoc -I . --python_betterproto_out=lib example.proto +``` + This will generate `lib/hello/__init__.py` which looks like: ```python @@ -319,7 +326,7 @@ datetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc) ## Development - _Join us on [Slack](https://join.slack.com/t/betterproto/shared_invite/zt-f0n0uolx-iN8gBNrkPxtKHTLpG3o1OQ)!_ -- _See how you can help → [Contributing](CONTRIBUTING.md)_ +- _See how you can help → [Contributing](.github/CONTRIBUTING.md)_ ### Requirements diff --git a/asv.conf.json b/asv.conf.json new file mode 100644 index 000000000..58a43420b --- /dev/null +++ b/asv.conf.json @@ -0,0 +1,157 @@ +{ + // The version of the config file format. Do not change, unless + // you know what you are doing. + "version": 1, + + // The name of the project being benchmarked + "project": "python-betterproto", + + // The project's homepage + "project_url": "https://github.com/danielgtaylor/python-betterproto", + + // The URL or local path of the source code repository for the + // project being benchmarked + "repo": ".", + + // The Python project's subdirectory in your repo. If missing or + // the empty string, the project is assumed to be located at the root + // of the repository. + // "repo_subdir": "", + + // Customizable commands for building, installing, and + // uninstalling the project. See asv.conf.json documentation. + // + "install_command": ["python -m pip install ."], + "uninstall_command": ["return-code=any python -m pip uninstall -y {project}"], + "build_command": ["python -m pip wheel -w {build_cache_dir} {build_dir}"], + + // List of branches to benchmark. If not provided, defaults to "master" + // (for git) or "default" (for mercurial). + // "branches": ["master"], // for git + // "branches": ["default"], // for mercurial + + // The DVCS being used. If not set, it will be automatically + // determined from "repo" by looking at the protocol in the URL + // (if remote), or by looking for special directories, such as + // ".git" (if local). + // "dvcs": "git", + + // The tool to use to create environments. May be "conda", + // "virtualenv" or other value depending on the plugins in use. + // If missing or the empty string, the tool will be automatically + // determined by looking for tools on the PATH environment + // variable. + "environment_type": "virtualenv", + + // timeout in seconds for installing any dependencies in environment + // defaults to 10 min + //"install_timeout": 600, + + // the base URL to show a commit for the project. + // "show_commit_url": "http://github.com/owner/project/commit/", + + // The Pythons you'd like to test against. If not provided, defaults + // to the current version of Python used to run `asv`. + // "pythons": ["2.7", "3.6"], + + // The list of conda channel names to be searched for benchmark + // dependency packages in the specified order + // "conda_channels": ["conda-forge", "defaults"], + + // The matrix of dependencies to test. Each key is the name of a + // package (in PyPI) and the values are version numbers. An empty + // list or empty string indicates to just test against the default + // (latest) version. null indicates that the package is to not be + // installed. If the package to be tested is only available from + // PyPi, and the 'environment_type' is conda, then you can preface + // the package name by 'pip+', and the package will be installed via + // pip (with all the conda available packages installed first, + // followed by the pip installed packages). + // + // "matrix": { + // "numpy": ["1.6", "1.7"], + // "six": ["", null], // test with and without six installed + // "pip+emcee": [""], // emcee is only available for install with pip. + // }, + + // Combinations of libraries/python versions can be excluded/included + // from the set to test. Each entry is a dictionary containing additional + // key-value pairs to include/exclude. + // + // An exclude entry excludes entries where all values match. The + // values are regexps that should match the whole string. + // + // An include entry adds an environment. Only the packages listed + // are installed. The 'python' key is required. The exclude rules + // do not apply to includes. + // + // In addition to package names, the following keys are available: + // + // - python + // Python version, as in the *pythons* variable above. + // - environment_type + // Environment type, as above. + // - sys_platform + // Platform, as in sys.platform. Possible values for the common + // cases: 'linux2', 'win32', 'cygwin', 'darwin'. + // + // "exclude": [ + // {"python": "3.2", "sys_platform": "win32"}, // skip py3.2 on windows + // {"environment_type": "conda", "six": null}, // don't run without six on conda + // ], + // + // "include": [ + // // additional env for python2.7 + // {"python": "2.7", "numpy": "1.8"}, + // // additional env if run on windows+conda + // {"platform": "win32", "environment_type": "conda", "python": "2.7", "libpython": ""}, + // ], + + // The directory (relative to the current directory) that benchmarks are + // stored in. If not provided, defaults to "benchmarks" + // "benchmark_dir": "benchmarks", + + // The directory (relative to the current directory) to cache the Python + // environments in. If not provided, defaults to "env" + "env_dir": ".asv/env", + + // The directory (relative to the current directory) that raw benchmark + // results are stored in. If not provided, defaults to "results". + "results_dir": ".asv/results", + + // The directory (relative to the current directory) that the html tree + // should be written to. If not provided, defaults to "html". + "html_dir": ".asv/html", + + // The number of characters to retain in the commit hashes. + // "hash_length": 8, + + // `asv` will cache results of the recent builds in each + // environment, making them faster to install next time. This is + // the number of builds to keep, per environment. + // "build_cache_size": 2, + + // The commits after which the regression search in `asv publish` + // should start looking for regressions. Dictionary whose keys are + // regexps matching to benchmark names, and values corresponding to + // the commit (exclusive) after which to start looking for + // regressions. The default is to start from the first commit + // with results. If the commit is `null`, regression detection is + // skipped for the matching benchmark. + // + // "regressions_first_commits": { + // "some_benchmark": "352cdf", // Consider regressions only after this commit + // "another_benchmark": null, // Skip regression detection altogether + // }, + + // The thresholds for relative change in results, after which `asv + // publish` starts reporting regressions. Dictionary of the same + // form as in ``regressions_first_commits``, with values + // indicating the thresholds. If multiple entries match, the + // maximum is taken. If no entry matches, the default is 5%. + // + // "regressions_thresholds": { + // "some_benchmark": 0.01, // Threshold of 1% + // "another_benchmark": 0.5, // Threshold of 50% + // }, +} diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/benchmarks/__init__.py @@ -0,0 +1 @@ + diff --git a/benchmarks/benchmarks.py b/benchmarks/benchmarks.py new file mode 100644 index 000000000..107ce01f1 --- /dev/null +++ b/benchmarks/benchmarks.py @@ -0,0 +1,56 @@ +import betterproto + + +class TestMessage(betterproto.Message): + foo: int = betterproto.uint32_field(0) + bar: str = betterproto.string_field(1) + baz: float = betterproto.float_field(2) + + +class BenchMessage: + """Test creation and usage a proto message.""" + + def setup(self): + self.cls = TestMessage + self.instance = TestMessage() + self.instance_filled = TestMessage(0, "test", 0.0) + + def time_overhead(self): + """Overhead in class definition.""" + + class Message(betterproto.Message): + foo: int = betterproto.uint32_field(0) + bar: str = betterproto.string_field(1) + baz: float = betterproto.float_field(2) + + def time_instantiation(self): + """Time instantiation""" + self.cls() + + def time_attribute_access(self): + """Time to access an attribute""" + self.instance.foo + self.instance.bar + self.instance.baz + + def time_init_with_values(self): + """Time to set an attribute""" + self.cls(0, "test", 0.0) + + def time_attribute_setting(self): + """Time to set attributes""" + self.instance.foo = 0 + self.instance.bar = "test" + self.instance.baz = 0.0 + + def time_serialize(self): + """Time serializing a message to wire.""" + bytes(self.instance_filled) + + +class MemSuite: + def setup(self): + self.cls = TestMessage + + def mem_instance(self): + return self.cls() diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 000000000..9f99b51d8 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,31 @@ +.. currentmodule:: betterproto + +API reference +============= + +The following document outlines betterproto's api. **None** of these classes should be +extended by the user manually. + + +Message +-------- + +.. autoclass:: betterproto.Message + :members: + :special-members: __bytes__, __bool__ + + +.. autofunction:: betterproto.serialized_on_wire + +.. autofunction:: betterproto.which_one_of + + +Enumerations +------------- + +.. autoclass:: betterproto.Enum() + :members: + + +.. autoclass:: betterproto.Casing() + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..564208fec --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,60 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. + +import pathlib + +import toml + + +# -- Project information ----------------------------------------------------- + +project = "betterproto" +copyright = "2019 Daniel G. Taylor" +author = "danielgtaylor" +pyproject = toml.load(open(pathlib.Path(__file__).parent.parent / "pyproject.toml")) + + +# The full version, including alpha/beta/rc tags. +release = pyproject["tool"]["poetry"]["version"] + + +# -- General configuration --------------------------------------------------- + +# 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.intersphinx", + "sphinx.ext.napoleon", +] + +autodoc_member_order = "bysource" +autodoc_typehints = "none" + +extlinks = { + "issue": ("https://github.com/danielgtaylor/python-betterproto/issues/%s", "GH-"), +} + +# Links used for cross-referencing stuff in other documentation +intersphinx_mapping = { + "py": ("https://docs.python.org/3", None), +} + + +# -- Options for HTML output ------------------------------------------------- + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "friendly" + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. + +html_theme = "sphinx_rtd_theme" diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..dfaa8fcef --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,33 @@ +Welcome to betterproto's documentation! +======================================= + +betterproto is a protobuf compiler and interpreter. It improves the experience of using +Protobuf and gRPC in Python, by generating readable, understandable, and idiomatic +Python code, using modern language features. + + +Features: +~~~~~~~~~ + +- Generated messages are both binary & JSON serializable +- Messages use relevant python types, e.g. ``Enum``, ``datetime`` and ``timedelta`` + objects +- ``async``/``await`` support for gRPC Clients +- Generates modern, readable, idiomatic python code + +Contents: +~~~~~~~~~ + +.. toctree:: + :maxdepth: 2 + + quick-start + api + migrating + + +If you still can't find what you're looking for, try in one of the following pages: + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/migrating.rst b/docs/migrating.rst new file mode 100644 index 000000000..0f18eac5f --- /dev/null +++ b/docs/migrating.rst @@ -0,0 +1,157 @@ +Migrating Guide +=============== + +Google's protocolbuffers +------------------------ + +betterproto has a mostly 1 to 1 drop in replacement for Google's protocolbuffers (after +regenerating your protobufs of course) although there are some minor differences. + +.. note:: + + betterproto implements the same basic methods including: + + - :meth:`betterproto.Message.FromString` + - :meth:`betterproto.Message.SerializeToString` + + for compatibility purposes, however it is important to note that these are + effectively aliases for :meth:`betterproto.Message.parse` and + :meth:`betterproto.Message.__bytes__` respectively. + + +Determining if a message was sent +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes it is useful to be able to determine whether a message has been sent on +the wire. This is how the Google wrapper types work to let you know whether a value is +unset (set as the default/zero value), or set as something else, for example. + +Use ``betterproto.serialized_on_wire(message)`` to determine if it was sent. This is +a little bit different from the official Google generated Python code, and it lives +outside the generated ``Message`` class to prevent name clashes. Note that it only +supports Proto 3 and thus can only be used to check if ``Message`` fields are set. +You cannot check if a scalar was sent on the wire. + +.. code-block:: python + + # Old way (official Google Protobuf package) + >>> mymessage.HasField('myfield') + True + + # New way (this project) + >>> betterproto.serialized_on_wire(mymessage.myfield) + True + + +One-of Support +~~~~~~~~~~~~~~ + +Protobuf supports grouping fields in a oneof clause. Only one of the fields in the group +may be set at a given time. For example, given the proto: + +.. code-block:: proto + + syntax = "proto3"; + + message Test { + oneof foo { + bool on = 1; + int32 count = 2; + string name = 3; + } + } + +You can use ``betterproto.which_one_of(message, group_name)`` to determine which of the +fields was set. It returns a tuple of the field name and value, or a blank string and +``None`` if unset. Again this is a little different than the official Google code +generator: + +.. code-block:: python + + # Old way (official Google protobuf package) + >>> message.WhichOneof("group") + "foo" + + # New way (this project) + >>> betterproto.which_one_of(message, "group") + ("foo", "foo's value") + + +Well-Known Google Types +~~~~~~~~~~~~~~~~~~~~~~~ + +Google provides several well-known message types like a timestamp, duration, and several +wrappers used to provide optional zero value support. Each of these has a special JSON +representation and is handled a little differently from normal messages. The Python +mapping for these is as follows: + ++-------------------------------+-----------------------------------------------+--------------------------+ +| ``Google Message`` | ``Python Type`` | ``Default`` | ++===============================+===============================================+==========================+ +| ``google.protobuf.duration`` | :class:`datetime.timedelta` | ``0`` | ++-------------------------------+-----------------------------------------------+--------------------------+ +| ``google.protobuf.timestamp`` | ``Timezone-aware`` :class:`datetime.datetime` | ``1970-01-01T00:00:00Z`` | ++-------------------------------+-----------------------------------------------+--------------------------+ +| ``google.protobuf.*Value`` | ``Optional[...]``/``None`` | ``None`` | ++-------------------------------+-----------------------------------------------+--------------------------+ +| ``google.protobuf.*`` | ``betterproto.lib.google.protobuf.*`` | ``None`` | ++-------------------------------+-----------------------------------------------+--------------------------+ + + +For the wrapper types, the Python type corresponds to the wrapped type, e.g. +``google.protobuf.BoolValue`` becomes ``Optional[bool]`` while +``google.protobuf.Int32Value`` becomes ``Optional[int]``. All of the optional values +default to None, so don't forget to check for that possible state. + +Given: + +.. code-block:: proto + + syntax = "proto3"; + + import "google/protobuf/duration.proto"; + import "google/protobuf/timestamp.proto"; + import "google/protobuf/wrappers.proto"; + + message Test { + google.protobuf.BoolValue maybe = 1; + google.protobuf.Timestamp ts = 2; + google.protobuf.Duration duration = 3; + } + +You can use it as such: + +.. code-block:: python + + >>> t = Test().from_dict({"maybe": True, "ts": "2019-01-01T12:00:00Z", "duration": "1.200s"}) + >>> t + Test(maybe=True, ts=datetime.datetime(2019, 1, 1, 12, 0, tzinfo=datetime.timezone.utc), duration=datetime.timedelta(seconds=1, microseconds=200000)) + + >>> t.ts - t.duration + datetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc) + + >>> t.ts.isoformat() + '2019-01-01T12:00:00+00:00' + + >>> t.maybe = None + >>> t.to_dict() + {'ts': '2019-01-01T12:00:00Z', 'duration': '1.200s'} + + +[1.2.5] to [2.0.0b1] +-------------------- + +Updated package structures +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Generated code now strictly follows the *package structure* of the ``.proto`` files. +Consequently ``.proto`` files without a package will be combined in a single +``__init__.py`` file. To avoid overwriting existing ``__init__.py`` files, its best +to compile into a dedicated subdirectory. + +Upgrading: + +- Remove your previously compiled ``.py`` files. +- Create a new *empty* directory, e.g. ``generated`` or ``lib/generated/proto`` etc. +- Regenerate your python files into this directory +- Update import statements, e.g. ``import ExampleMessage from generated`` diff --git a/docs/quick-start.rst b/docs/quick-start.rst new file mode 100644 index 000000000..c731ca20e --- /dev/null +++ b/docs/quick-start.rst @@ -0,0 +1,192 @@ +Getting Started +=============== + +Installation +++++++++++++ + +Installation from PyPI is as simple as running: + +.. code-block:: sh + + python3 -m pip install -U betterproto + +If you are using Windows, then the following should be used instead: + +.. code-block:: sh + + py -3 -m pip install -U betterproto + +To include the protoc plugin, install betterproto[compiler] instead of betterproto, +e.g. + +.. code-block:: sh + + python3 -m pip install -U "betterproto[compiler]" + +Compiling proto files ++++++++++++++++++++++ + + +Given you installed the compiler and have a proto file, e.g ``example.proto``: + +.. code-block:: proto + + syntax = "proto3"; + + package hello; + + // Greeting represents a message you can tell a user. + message Greeting { + string message = 1; + } + +To compile the proto you would run the following: + +You can run the following to invoke protoc directly: + +.. code-block:: sh + + mkdir hello + protoc -I . --python_betterproto_out=lib example.proto + +or run the following to invoke protoc via grpcio-tools: + +.. code-block:: sh + + pip install grpcio-tools + python -m grpc_tools.protoc -I . --python_betterproto_out=lib example.proto + + +This will generate ``lib/__init__.py`` which looks like: + +.. code-block:: python + + # Generated by the protocol buffer compiler. DO NOT EDIT! + # sources: example.proto + # plugin: python-betterproto + from dataclasses import dataclass + + import betterproto + + + @dataclass + class Greeting(betterproto.Message): + """Greeting represents a message you can tell a user.""" + + message: str = betterproto.string_field(1) + + +Then to use it: + +.. code-block:: python + + >>> from lib import Greeting + + >>> test = Greeting() + >>> test + Greeting(message='') + + >>> test.message = "Hey!" + >>> test + Greeting(message="Hey!") + + >>> bytes(test) + b'\n\x04Hey!' + >>> Greeting().parse(serialized) + Greeting(message="Hey!") + + +Async gRPC Support +++++++++++++++++++ + +The generated code includes `grpclib `_ based +stub (client) classes for rpc services declared in the input proto files. +It is enabled by default. + + +Given a service definition similar to the one below: + +.. code-block:: proto + + syntax = "proto3"; + + package echo; + + message EchoRequest { + string value = 1; + // Number of extra times to echo + uint32 extra_times = 2; + } + + message EchoResponse { + repeated string values = 1; + } + + message EchoStreamResponse { + string value = 1; + } + + service Echo { + rpc Echo(EchoRequest) returns (EchoResponse); + rpc EchoStream(EchoRequest) returns (stream EchoStreamResponse); + } + +The generated client can be used like so: + +.. code-block:: python + + import asyncio + from grpclib.client import Channel + import echo + + + async def main(): + channel = Channel(host="127.0.0.1", port=50051) + service = echo.EchoStub(channel) + response = await service.echo(value="hello", extra_times=1) + print(response) + + async for response in service.echo_stream(value="hello", extra_times=1): + print(response) + + # don't forget to close the channel when you're done! + channel.close() + + asyncio.run(main()) # python 3.7 only + + # outputs + EchoResponse(values=['hello', 'hello']) + EchoStreamResponse(value='hello') + EchoStreamResponse(value='hello') + + +JSON +++++ +Message objects include :meth:`betterproto.Message.to_json` and +:meth:`betterproto.Message.from_json` methods for JSON (de)serialisation, and +:meth:`betterproto.Message.to_dict`, :meth:`betterproto.Message.from_dict` for +converting back and forth from JSON serializable dicts. + +For compatibility the default is to convert field names to +:attr:`betterproto.Casing.CAMEL`. You can control this behavior by passing a +different casing value, e.g: + +.. code-block:: python + + @dataclass + class MyMessage(betterproto.Message): + a_long_field_name: str = betterproto.string_field(1) + + + >>> test = MyMessage(a_long_field_name="Hello World!") + >>> test.to_dict(betterproto.Casing.SNAKE) + {"a_long_field_name": "Hello World!"} + >>> test.to_dict(betterproto.Casing.CAMEL) + {"aLongFieldName": "Hello World!"} + + >>> test.to_json(indent=2) + '{\n "aLongFieldName": "Hello World!"\n}' + + >>> test.from_dict({"aLongFieldName": "Goodbye World!"}) + >>> test.a_long_field_name + "Goodbye World!" diff --git a/docs/upgrading.md b/docs/upgrading.md deleted file mode 100644 index 84534a35a..000000000 --- a/docs/upgrading.md +++ /dev/null @@ -1,16 +0,0 @@ -# Upgrade Guide - -## [1.2.5] to [2.0.0b1] - -### Updated package structures - -Generated code now strictly follows the *package structure* of the `.proto` files. -Consequently `.proto` files without a package will be combined in a single `__init__.py` file. -To avoid overwriting existing `__init__.py` files, its best to compile into a dedicated subdirectory. - -Upgrading: - -- Remove your previously compiled `.py` files. -- Create a new *empty* directory, e.g. `generated` or `lib/generated/proto` etcetera. -- Regenerate your python files into this directory -- Update import statements, e.g. `import ExampleMessage from generated` \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 7aa42ecdf..09b957bf9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,49 +1,87 @@ [[package]] -category = "main" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -name = "appdirs" +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" optional = false python-versions = "*" + +[[package]] +name = "appdirs" version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" [[package]] +name = "asv" +version = "0.4.2" +description = "Airspeed Velocity: A simple Python history benchmarking tool" category = "dev" -description = "Atomic file writes." -marker = "sys_platform == \"win32\"" -name = "atomicwrites" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.0" + +[package.extras] +hg = ["python-hglib (>=1.5)"] + +[package.dependencies] +six = ">=1.4" [[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." category = "dev" -description = "Classes Without Boilerplate" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +marker = "sys_platform == \"win32\"" + +[[package]] name = "attrs" +version = "20.2.0" +description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.1.0" [package.extras] dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "babel" +version = "2.8.0" +description = "Internationalization utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytz = ">=2015.7" [[package]] -category = "main" -description = "Backport of Python 3.7's datetime.fromisoformat" -marker = "python_version < \"3.7\"" name = "backports-datetime-fromisoformat" +version = "1.0.0" +description = "Backport of Python 3.7's datetime.fromisoformat" +category = "main" optional = false python-versions = "*" -version = "1.0.0" +marker = "python_version < \"3.7\"" [[package]] -category = "main" -description = "The uncompromising code formatter." name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "main" optional = false python-versions = ">=3.6" -version = "20.8b1" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [package.dependencies] appdirs = "*" @@ -56,31 +94,32 @@ typed-ast = ">=1.4.0" typing-extensions = ">=3.7.4" [package.dependencies.dataclasses] -python = "<3.7" version = ">=0.6" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +python = "<3.7" [[package]] -category = "dev" -description = "A thin, practical wrapper around terminal coloring, styling, and positioning" name = "blessings" +version = "1.7" +description = "A thin, practical wrapper around terminal coloring, styling, and positioning" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.7" [package.dependencies] six = "*" [[package]] -category = "dev" -description = "Fancy Interface to the Python Interpreter" name = "bpython" +version = "0.19" +description = "Fancy Interface to the Python Interpreter" +category = "dev" optional = false python-versions = "*" -version = "0.19" + +[package.extras] +jedi = ["jedi"] +urwid = ["urwid"] +watch = ["watchdog"] [package.dependencies] curtsies = ">=0.1.18" @@ -89,372 +128,387 @@ pygments = "*" requests = "*" six = ">=1.5" -[package.extras] -jedi = ["jedi"] -urwid = ["urwid"] -watch = ["watchdog"] - [[package]] -category = "dev" -description = "Python package for providing Mozilla's CA Bundle." name = "certifi" +version = "2020.6.20" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" optional = false python-versions = "*" -version = "2020.6.20" [[package]] -category = "dev" -description = "Universal encoding detector for Python 2 and 3" name = "chardet" +version = "3.0.4" +description = "Universal encoding detector for Python 2 and 3" +category = "dev" optional = false python-versions = "*" -version = "3.0.4" [[package]] -category = "main" -description = "Composable command line interface toolkit" name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2" [[package]] -category = "dev" -description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" name = "colorama" +version = "0.4.3" +description = "Cross-platform colored terminal text." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.3" +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" [[package]] -category = "dev" -description = "Code coverage measurement for Python" name = "coverage" +version = "5.3" +description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.2.1" [package.extras] toml = ["toml"] [[package]] -category = "dev" -description = "Curses-like terminal wrapper, with colored strings!" name = "curtsies" +version = "0.3.4" +description = "Curses-like terminal wrapper, with colored strings!" +category = "dev" optional = false python-versions = "*" -version = "0.3.4" [package.dependencies] blessings = ">=1.5" wcwidth = ">=0.1.4" [[package]] -category = "main" -description = "A backport of the dataclasses module for Python 3.6" -marker = "python_version >= \"3.6\" and python_version < \"3.7\" or python_version < \"3.7\"" name = "dataclasses" +version = "0.7" +description = "A backport of the dataclasses module for Python 3.6" +category = "main" optional = false python-versions = ">=3.6, <3.7" -version = "0.7" +marker = "python_version >= \"3.6\" and python_version < \"3.7\" or python_version < \"3.7\"" [[package]] -category = "dev" -description = "Distribution utilities" name = "distlib" +version = "0.3.1" +description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" -version = "0.3.1" [[package]] +name = "docutils" +version = "0.16" +description = "Docutils -- Python Documentation Utilities" category = "dev" -description = "A platform independent file lock." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." +category = "dev" optional = false python-versions = "*" -version = "3.0.12" [[package]] -category = "dev" -description = "Lightweight in-process concurrent programming" name = "greenlet" +version = "0.4.17" +description = "Lightweight in-process concurrent programming" +category = "dev" optional = false python-versions = "*" -version = "0.4.16" [[package]] -category = "dev" -description = "HTTP/2-based RPC framework" name = "grpcio" +version = "1.32.0" +description = "HTTP/2-based RPC framework" +category = "dev" optional = false python-versions = "*" -version = "1.31.0" + +[package.extras] +protobuf = ["grpcio-tools (>=1.32.0)"] [package.dependencies] six = ">=1.5.2" -[package.extras] -protobuf = ["grpcio-tools (>=1.31.0)"] - [[package]] -category = "dev" -description = "Protobuf code generator for gRPC" name = "grpcio-tools" +version = "1.32.0" +description = "Protobuf code generator for gRPC" +category = "dev" optional = false python-versions = "*" -version = "1.31.0" [package.dependencies] -grpcio = ">=1.31.0" +grpcio = ">=1.32.0" protobuf = ">=3.5.0.post1,<4.0dev" [[package]] -category = "main" -description = "Pure-Python gRPC implementation for asyncio" name = "grpclib" +version = "0.4.1" +description = "Pure-Python gRPC implementation for asyncio" +category = "main" optional = false python-versions = ">=3.6" -version = "0.3.2" [package.dependencies] -h2 = "*" +h2 = ">=3.1.0,<5" multidict = "*" [package.dependencies.dataclasses] -python = "<3.7" version = "*" +python = "<3.7" [[package]] -category = "main" -description = "HTTP/2 State-Machine based protocol implementation" name = "h2" +version = "3.2.0" +description = "HTTP/2 State-Machine based protocol implementation" +category = "main" optional = false python-versions = "*" -version = "3.2.0" [package.dependencies] hpack = ">=3.0,<4" hyperframe = ">=5.2.0,<6" [[package]] -category = "main" -description = "Pure-Python HPACK header compression" name = "hpack" +version = "3.0.0" +description = "Pure-Python HPACK header compression" +category = "main" optional = false python-versions = "*" -version = "3.0.0" [[package]] -category = "main" -description = "HTTP/2 framing layer for Python" name = "hyperframe" +version = "5.2.0" +description = "HTTP/2 framing layer for Python" +category = "main" optional = false python-versions = "*" -version = "5.2.0" [[package]] -category = "dev" -description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.10" [[package]] +name = "imagesize" +version = "1.2.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "dev" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] name = "importlib-metadata" +version = "1.7.0" +description = "Read metadata from Python packages" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.7.0" - -[package.dependencies] -zipp = ">=0.5" +marker = "python_version < \"3.8\"" [package.extras] docs = ["sphinx", "rst.linker"] testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] +[package.dependencies] +zipp = ">=0.5" + [[package]] -category = "dev" -description = "Read resources from Python packages" -marker = "python_version < \"3.7\"" name = "importlib-resources" +version = "3.0.0" +description = "Read resources from Python packages" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "3.0.0" +marker = "python_version < \"3.7\"" + +[package.extras] +docs = ["sphinx", "rst.linker", "jaraco.packaging"] [package.dependencies] [package.dependencies.zipp] -python = "<3.8" version = ">=0.4" - -[package.extras] -docs = ["sphinx", "rst.linker", "jaraco.packaging"] +python = "<3.8" [[package]] -category = "main" -description = "A very fast and expressive template engine." name = "jinja2" +version = "2.11.2" +description = "A very fast and expressive template engine." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.11.2" - -[package.dependencies] -MarkupSafe = ">=0.23" [package.extras] i18n = ["Babel (>=0.8)"] +[package.dependencies] +MarkupSafe = ">=0.23" + [[package]] -category = "main" -description = "Safely add untrusted strings to HTML/XML markup." name = "markupsafe" +version = "1.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.1.1" [[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" +version = "8.5.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" optional = false python-versions = ">=3.5" -version = "8.5.0" [[package]] -category = "main" -description = "multidict implementation" name = "multidict" +version = "4.7.6" +description = "multidict implementation" +category = "main" optional = false python-versions = ">=3.5" -version = "4.7.6" [[package]] -category = "dev" -description = "Optional static typing for Python" name = "mypy" +version = "0.770" +description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.5" -version = "0.770" + +[package.extras] +dmypy = ["psutil (>=4.0)"] [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" typed-ast = ">=1.4.0,<1.5.0" typing-extensions = ">=3.7.4" -[package.extras] -dmypy = ["psutil (>=4.0)"] - [[package]] -category = "main" -description = "Experimental type system extensions for programs checked with the mypy typechecker." name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "main" optional = false python-versions = "*" -version = "0.4.3" [[package]] -category = "dev" -description = "Core utilities for Python packages" name = "packaging" +version = "20.4" +description = "Core utilities for Python packages" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.4" [package.dependencies] pyparsing = ">=2.0.2" six = "*" [[package]] -category = "dev" -description = "Bring colors to your terminal." name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.2.0" [[package]] -category = "main" -description = "Utility library for gitignore style pattern matching of file paths." name = "pathspec" +version = "0.8.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.8.0" [[package]] -category = "dev" -description = "plugin and hook calling mechanisms for python" name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.1" + +[package.extras] +dev = ["pre-commit", "tox"] [package.dependencies] [package.dependencies.importlib-metadata] -python = "<3.8" version = ">=0.12" - -[package.extras] -dev = ["pre-commit", "tox"] +python = "<3.8" [[package]] -category = "dev" -description = "A task runner that works well with poetry." name = "poethepoet" +version = "0.5.0" +description = "A task runner that works well with poetry." +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "0.5.0" [package.dependencies] pastel = ">=0.2.0,<0.3.0" toml = ">=0.10.1,<0.11.0" [[package]] -category = "main" -description = "Protocol Buffers" name = "protobuf" +version = "3.13.0" +description = "Protocol Buffers" +category = "main" optional = false python-versions = "*" -version = "3.13.0" [package.dependencies] setuptools = "*" six = ">=1.9" [[package]] -category = "dev" -description = "library with cross-python path, ini-parsing, io, code, log facilities" name = "py" +version = "1.9.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.9.0" [[package]] -category = "dev" -description = "Pygments is a syntax highlighting package written in Python." name = "pygments" +version = "2.7.1" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" optional = false python-versions = ">=3.5" -version = "2.6.1" [[package]] -category = "dev" -description = "Python parsing module" name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.7" [[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" name = "pytest" +version = "5.4.3" +description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.5" -version = "5.4.3" + +[package.extras] +checkqa-mypy = ["mypy (v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [package.dependencies] atomicwrites = ">=1.0" @@ -467,71 +521,79 @@ py = ">=1.5.0" wcwidth = "*" [package.dependencies.importlib-metadata] -python = "<3.8" version = ">=0.12" - -[package.extras] -checkqa-mypy = ["mypy (v0.761)"] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +python = "<3.8" [[package]] -category = "dev" -description = "Pytest support for asyncio." name = "pytest-asyncio" +version = "0.12.0" +description = "Pytest support for asyncio." +category = "dev" optional = false python-versions = ">= 3.5" -version = "0.12.0" - -[package.dependencies] -pytest = ">=5.4.0" [package.extras] testing = ["async_generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"] +[package.dependencies] +pytest = ">=5.4.0" + [[package]] -category = "dev" -description = "Pytest plugin for measuring coverage." name = "pytest-cov" +version = "2.10.1" +description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.10.1" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] [package.dependencies] coverage = ">=4.4" pytest = ">=4.6" -[package.extras] -testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] - [[package]] -category = "dev" -description = "Thin-wrapper around the mock package for easier use with pytest" name = "pytest-mock" +version = "3.3.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" optional = false python-versions = ">=3.5" -version = "3.3.1" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] [package.dependencies] pytest = ">=5.0" -[package.extras] -dev = ["pre-commit", "tox", "pytest-asyncio"] +[[package]] +name = "pytz" +version = "2020.1" +description = "World timezone definitions, modern and historical" +category = "dev" +optional = false +python-versions = "*" [[package]] -category = "main" -description = "Alternative regular expression module, to replace re." name = "regex" +version = "2020.7.14" +description = "Alternative regular expression module, to replace re." +category = "main" optional = false python-versions = "*" -version = "2020.7.14" [[package]] -category = "dev" -description = "Python HTTP for Humans." name = "requests" +version = "2.24.0" +description = "Python HTTP for Humans." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.24.0" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] [package.dependencies] certifi = ">=2017.4.17" @@ -539,33 +601,158 @@ chardet = ">=3.0.2,<4" idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" -[package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] - [[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" [[package]] -category = "main" -description = "Python Library for Tom's Obvious, Minimal Language" -name = "toml" +name = "snowballstemmer" +version = "2.0.0" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +category = "dev" optional = false python-versions = "*" -version = "0.10.1" [[package]] +name = "sphinx" +version = "3.1.2" +description = "Python documentation generator" category = "dev" -description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] +test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = ">=0.3.5" +docutils = ">=0.12" +imagesize = "*" +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +setuptools = "*" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[[package]] +name = "sphinx-rtd-theme" +version = "0.5.0" +description = "Read the Docs theme for Sphinx" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] + +[package.dependencies] +sphinx = "*" + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "1.0.3" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.4" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "toml" +version = "0.10.1" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = "*" + +[[package]] name = "tox" +version = "3.20.0" +description = "tox is a generic virtualenv management and test command line tool" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "3.19.0" + +[package.extras] +docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)"] [package.dependencies] colorama = ">=0.4.1" @@ -578,36 +765,32 @@ toml = ">=0.9.4" virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" [package.dependencies.importlib-metadata] -python = "<3.8" version = ">=0.12,<2" - -[package.extras] -docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)"] +python = "<3.8" [[package]] -category = "main" -description = "a fork of Python 2 and 3 ast modules with type comment support" name = "typed-ast" +version = "1.4.1" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "main" optional = false python-versions = "*" -version = "1.4.1" [[package]] -category = "main" -description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" optional = false python-versions = "*" -version = "3.7.4.3" [[package]] -category = "dev" -description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" +version = "1.25.10" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.10" [package.extras] brotli = ["brotlipy (>=0.6.0)"] @@ -615,12 +798,16 @@ secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0 socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] -category = "dev" -description = "Virtual Python Environment builder" name = "virtualenv" +version = "20.0.31" +description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "20.0.31" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] [package.dependencies] appdirs = ">=1.4.3,<2" @@ -629,58 +816,65 @@ filelock = ">=3.0.0,<4" six = ">=1.9.0,<2" [package.dependencies.importlib-metadata] -python = "<3.8" version = ">=0.12,<2" +python = "<3.8" [package.dependencies.importlib-resources] -python = "<3.7" version = ">=1.0" - -[package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] -testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] +python = "<3.7" [[package]] -category = "dev" -description = "Measures the displayed width of unicode strings in a terminal" name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" optional = false python-versions = "*" -version = "0.2.5" [[package]] -category = "dev" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" name = "zipp" +version = "3.2.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" optional = false python-versions = ">=3.6" -version = "3.1.0" +marker = "python_version < \"3.8\"" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] +testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [extras] compiler = ["black", "jinja2", "protobuf"] [metadata] -content-hash = "6bd0923315785f42562ab1c7f3de73a20858fd7ebd6cc089f916cfc4719eac83" lock-version = "1.0" python-versions = "^3.6" +content-hash = "a8e4b87ff691fd815c51637f7fd24606c9ccdfbb7d9466a01c844fdfc58922c0" [metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] +asv = [ + {file = "asv-0.4.2.tar.gz", hash = "sha256:9134f56b7a2f465420f17b5bb0dee16047a70f01029c996b7ab3f197de2d0779"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, - {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, + {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, + {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, +] +babel = [ + {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, + {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, ] backports-datetime-fromisoformat = [ {file = "backports-datetime-fromisoformat-1.0.0.tar.gz", hash = "sha256:9577a2a9486cd7383a5f58b23bb8e81cf0821dbbc0eb7c87d3fa198c1df40f5c"}, @@ -715,40 +909,40 @@ colorama = [ {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] coverage = [ - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, - {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, - {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, - {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, - {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, - {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, - {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, - {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, - {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, - {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, - {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, - {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, - {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, - {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, - {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, - {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, - {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, - {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, - {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, + {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"}, + {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"}, + {file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"}, + {file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"}, + {file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"}, + {file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"}, + {file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"}, + {file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"}, + {file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"}, + {file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"}, + {file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"}, + {file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"}, + {file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"}, + {file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"}, + {file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"}, + {file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"}, + {file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"}, + {file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"}, + {file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"}, + {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"}, ] curtsies = [ {file = "curtsies-0.3.4-py2.py3-none-any.whl", hash = "sha256:068db8e5d8a2f23b765d648a66dfa9445cf2412177126ae946a7357ade992640"}, @@ -762,113 +956,118 @@ distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, ] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] greenlet = [ - {file = "greenlet-0.4.16-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872"}, - {file = "greenlet-0.4.16-cp27-cp27m-win32.whl", hash = "sha256:df7de669cbf21de4b04a3ffc9920bc8426cab4c61365fa84d79bf97401a8bef7"}, - {file = "greenlet-0.4.16-cp27-cp27m-win_amd64.whl", hash = "sha256:1429dc183b36ec972055e13250d96e174491559433eb3061691b446899b87384"}, - {file = "greenlet-0.4.16-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5ea034d040e6ab1d2ae04ab05a3f37dbd719c4dee3804b13903d4cc794b1336e"}, - {file = "greenlet-0.4.16-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c196a5394c56352e21cb7224739c6dd0075b69dd56f758505951d1d8d68cf8a9"}, - {file = "greenlet-0.4.16-cp35-cp35m-win32.whl", hash = "sha256:1000038ba0ea9032948e2156a9c15f5686f36945e8f9906e6b8db49f358e7b52"}, - {file = "greenlet-0.4.16-cp35-cp35m-win_amd64.whl", hash = "sha256:1b805231bfb7b2900a16638c3c8b45c694334c811f84463e52451e00c9412691"}, - {file = "greenlet-0.4.16-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e5db19d4a7d41bbeb3dd89b49fc1bc7e6e515b51bbf32589c618655a0ebe0bf0"}, - {file = "greenlet-0.4.16-cp36-cp36m-win32.whl", hash = "sha256:eac2a3f659d5f41d6bbfb6a97733bc7800ea5e906dc873732e00cebb98cec9e4"}, - {file = "greenlet-0.4.16-cp36-cp36m-win_amd64.whl", hash = "sha256:7eed31f4efc8356e200568ba05ad645525f1fbd8674f1e5be61a493e715e3873"}, - {file = "greenlet-0.4.16-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:682328aa576ec393c1872615bcb877cf32d800d4a2f150e1a5dc7e56644010b1"}, - {file = "greenlet-0.4.16-cp37-cp37m-win32.whl", hash = "sha256:3a35e33902b2e6079949feed7a2dafa5ac6f019da97bd255842bb22de3c11bf5"}, - {file = "greenlet-0.4.16-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b2a984bbfc543d144d88caad6cc7ff4a71be77102014bd617bd88cfb038727"}, - {file = "greenlet-0.4.16-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d83c1d38658b0f81c282b41238092ed89d8f93c6e342224ab73fb39e16848721"}, - {file = "greenlet-0.4.16-cp38-cp38-win32.whl", hash = "sha256:e695ac8c3efe124d998230b219eb51afb6ef10524a50b3c45109c4b77a8a3a92"}, - {file = "greenlet-0.4.16-cp38-cp38-win_amd64.whl", hash = "sha256:133ba06bad4e5f2f8bf6a0ac434e0fd686df749a86b3478903b92ec3a9c0c90b"}, - {file = "greenlet-0.4.16.tar.gz", hash = "sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c"}, + {file = "greenlet-0.4.17-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:75e4c27188f28149b74e7685809f9227410fd15432a4438fc48627f518577fa5"}, + {file = "greenlet-0.4.17-cp27-cp27m-win32.whl", hash = "sha256:3af587e9813f9bd8be9212722321a5e7be23b2bc37e6323a90e592ab0c2ef117"}, + {file = "greenlet-0.4.17-cp27-cp27m-win_amd64.whl", hash = "sha256:ccd62f09f90b2730150d82f2f2ffc34d73c6ce7eac234aed04d15dc8a3023994"}, + {file = "greenlet-0.4.17-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:13037e2d7ab2145300676852fa069235512fdeba4ed1e3bb4b0677a04223c525"}, + {file = "greenlet-0.4.17-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:e495096e3e2e8f7192afb6aaeba19babc4fb2bdf543d7b7fed59e00c1df7f170"}, + {file = "greenlet-0.4.17-cp35-cp35m-win32.whl", hash = "sha256:124a3ae41215f71dc91d1a3d45cbf2f84e46b543e5d60b99ecc20e24b4c8f272"}, + {file = "greenlet-0.4.17-cp35-cp35m-win_amd64.whl", hash = "sha256:5494e3baeacc371d988345fbf8aa4bd15555b3077c40afcf1994776bb6d77eaf"}, + {file = "greenlet-0.4.17-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bee111161420f341a346731279dd976be161b465c1286f82cc0779baf7b729e8"}, + {file = "greenlet-0.4.17-cp36-cp36m-win32.whl", hash = "sha256:ac85db59aa43d78547f95fc7b6fd2913e02b9e9b09e2490dfb7bbdf47b2a4914"}, + {file = "greenlet-0.4.17-cp36-cp36m-win_amd64.whl", hash = "sha256:4481002118b2f1588fa3d821936ffdc03db80ef21186b62b90c18db4ba5e743b"}, + {file = "greenlet-0.4.17-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:be7a79988b8fdc5bbbeaed69e79cfb373da9759242f1565668be4fb7f3f37552"}, + {file = "greenlet-0.4.17-cp37-cp37m-win32.whl", hash = "sha256:97f2b01ab622a4aa4b3724a3e1fba66f47f054c434fbaa551833fa2b41e3db51"}, + {file = "greenlet-0.4.17-cp37-cp37m-win_amd64.whl", hash = "sha256:d3436110ca66fe3981031cc6aff8cc7a40d8411d173dde73ddaa5b8445385e2d"}, + {file = "greenlet-0.4.17-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a34023b9eabb3525ee059f3bf33a417d2e437f7f17e341d334987d4091ae6072"}, + {file = "greenlet-0.4.17-cp38-cp38-win32.whl", hash = "sha256:e66a824f44892bc4ec66c58601a413419cafa9cec895e63d8da889c8a1a4fa4a"}, + {file = "greenlet-0.4.17-cp38-cp38-win_amd64.whl", hash = "sha256:47825c3a109f0331b1e54c1173d4e57fa000aa6c96756b62852bfa1af91cd652"}, + {file = "greenlet-0.4.17-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1023d7b43ca11264ab7052cb09f5635d4afdb43df55e0854498fc63070a0b206"}, + {file = "greenlet-0.4.17.tar.gz", hash = "sha256:41d8835c69a78de718e466dd0e6bfd4b46125f21a67c3ff6d76d8d8059868d6b"}, ] grpcio = [ - {file = "grpcio-1.31.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e8c3264b0fd728aadf3f0324471843f65bd3b38872bdab2a477e31ffb685dd5b"}, - {file = "grpcio-1.31.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5fb0923b16590bac338e92d98c7d8effb3cfad1d2e18c71bf86bde32c49cd6dd"}, - {file = "grpcio-1.31.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:58d7121f48cb94535a4cedcce32921d0d0a78563c7372a143dedeec196d1c637"}, - {file = "grpcio-1.31.0-cp27-cp27m-win32.whl", hash = "sha256:ea849210e7362559f326cbe603d5b8d8bb1e556e86a7393b5a8847057de5b084"}, - {file = "grpcio-1.31.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ba3e43cb984399064ffaa3c0997576e46a1e268f9da05f97cd9b272f0b59ee71"}, - {file = "grpcio-1.31.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:ebb2ca09fa17537e35508a29dcb05575d4d9401138a68e83d1c605d65e8a1770"}, - {file = "grpcio-1.31.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:292635f05b6ce33f87116951d0b3d8d330bdfc5cac74f739370d60981e8c256c"}, - {file = "grpcio-1.31.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:92e54ab65e782f227e751c7555918afaba8d1229601687e89b80c2b65d2f6642"}, - {file = "grpcio-1.31.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:013287f99c99b201aa8a5f6bc7918f616739b9be031db132d9e3b8453e95e151"}, - {file = "grpcio-1.31.0-cp35-cp35m-macosx_10_7_intel.whl", hash = "sha256:d2c5e05c257859febd03f5d81b5015e1946d6bcf475c7bf63ee99cea8ab0d590"}, - {file = "grpcio-1.31.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:c9016ab1eaf4e054099303287195f3746bd4e69f2631d040f9dca43e910a5408"}, - {file = "grpcio-1.31.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:baaa036540d7ace433bdf38a3fe5e41cf9f84cdf10a88bac805f678a7ca8ddcc"}, - {file = "grpcio-1.31.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:75e383053dccb610590aa53eed5278db5c09bf498d3b5105ce6c776478f59352"}, - {file = "grpcio-1.31.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:739a72abffbd36083ff7adbb862cf1afc1e311c35834bed9c0361d8e68b063e1"}, - {file = "grpcio-1.31.0-cp35-cp35m-win32.whl", hash = "sha256:f04c59d186af3157dc8811114130aaeae92e90a65283733f41de94eed484e1f7"}, - {file = "grpcio-1.31.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ef9fce98b6fe03874c2a6576b02aec1a0df25742cd67d1d7b75a49e30aa74225"}, - {file = "grpcio-1.31.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:08a9b648dbe8852ff94b73a1c96da126834c3057ba2301d13e8c4adff334c482"}, - {file = "grpcio-1.31.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c22b19abba63562a5a200e586b5bde39d26c8ec30c92e26d209d81182371693b"}, - {file = "grpcio-1.31.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:0397616355760cd8282ed5ea34d51830ae4cb6613b7e5f66bed3be5d041b8b9a"}, - {file = "grpcio-1.31.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:259240aab2603891553e17ad5b2655693df79e02a9b887ff605bdeb2fcd3dcc9"}, - {file = "grpcio-1.31.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:8ca26b489b5dc1e3d31807d329c23d6cb06fe40fbae25b0649b718947936e26a"}, - {file = "grpcio-1.31.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:bf39977282a79dc1b2765cc3402c0ada571c29a491caec6ed12c0993c1ec115e"}, - {file = "grpcio-1.31.0-cp36-cp36m-win32.whl", hash = "sha256:f5b0870b733bcb7b6bf05a02035e7aaf20f599d3802b390282d4c2309f825f1d"}, - {file = "grpcio-1.31.0-cp36-cp36m-win_amd64.whl", hash = "sha256:074871a184483d5cd0746fd01e7d214d3ee9d36e67e32a5786b0a21f29fb8304"}, - {file = "grpcio-1.31.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:220c46b1fc9c9a6fcca4caac398f08f0ed43cdd63c45b7458983c4a1575ef6df"}, - {file = "grpcio-1.31.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:7a11b1ebb3210f34913b8be6995936bf9ebc541a65ab69e75db5ce1fe5047e8f"}, - {file = "grpcio-1.31.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3c2aa6d7a5e5bf73fdb1715eee777efe06dd39df03383f1cc095b2fdb34883e6"}, - {file = "grpcio-1.31.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:e64bddd09842ef508d72ca354319b0eb126205d951e8ac3128fe9869bd563552"}, - {file = "grpcio-1.31.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5d7faa89992e015d245750ca9ac916c161bbf72777b2c60abc61da3fae41339e"}, - {file = "grpcio-1.31.0-cp37-cp37m-win32.whl", hash = "sha256:43d44548ad6ee738b941abd9f09e3b83a5c13f3e1410321023c3c148ba50e796"}, - {file = "grpcio-1.31.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bf00ab06ea4f89976288f4d6224d4aa120780e30c955d4f85c3214ada29b3ddf"}, - {file = "grpcio-1.31.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:344b50865914cc8e6d023457bffee9a640abb18f75d0f2bb519041961c748da9"}, - {file = "grpcio-1.31.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:63ee8e02d04272c3d103f44b4bce5d43ea757dd288673cea212d2f7da27967d2"}, - {file = "grpcio-1.31.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a9a7ae74cb3108e6457cf15532d4c300324b48fbcf3ef290bcd2835745f20510"}, - {file = "grpcio-1.31.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:64077e3a9a7cf2f59e6c76d503c8de1f18a76428f41a5b000dc53c48a0b772ff"}, - {file = "grpcio-1.31.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:8b42f0ac76be07a5fa31117a3388d754ad35ef05e2e34be185ca9ccbcfac2069"}, - {file = "grpcio-1.31.0-cp38-cp38-win32.whl", hash = "sha256:8002a89ea91c0078c15d3c0daf423fd4968946be78f08545e807ea9a5ff8054a"}, - {file = "grpcio-1.31.0-cp38-cp38-win_amd64.whl", hash = "sha256:0fa86ac4452602c79774783aa68979a1a7625ebb7eaabee2b6550b975b9d61e6"}, - {file = "grpcio-1.31.0.tar.gz", hash = "sha256:5043440c45c0a031f387e7f48527541c65d672005fb24cf18ef6857483557d39"}, + {file = "grpcio-1.32.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3afb058b6929eba07dba9ae6c5b555aa1d88cb140187d78cc510bd72d0329f28"}, + {file = "grpcio-1.32.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a8004b34f600a8a51785e46859cd88f3386ef67cccd1cfc7598e3d317608c643"}, + {file = "grpcio-1.32.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:e6786f6f7be0937614577edcab886ddce91b7c1ea972a07ef9972e9f9ecbbb78"}, + {file = "grpcio-1.32.0-cp27-cp27m-win32.whl", hash = "sha256:e467af6bb8f5843f5a441e124b43474715cfb3981264e7cd227343e826dcc3ce"}, + {file = "grpcio-1.32.0-cp27-cp27m-win_amd64.whl", hash = "sha256:1376a60f9bfce781b39973f100b5f67e657b5be479f2fd8a7d2a408fc61c085c"}, + {file = "grpcio-1.32.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:ce617e1c4a39131f8527964ac9e700eb199484937d7a0b3e52655a3ba50d5fb9"}, + {file = "grpcio-1.32.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:99bac0e2c820bf446662365df65841f0c2a55b0e2c419db86eaf5d162ddae73e"}, + {file = "grpcio-1.32.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6d869a3e8e62562b48214de95e9231c97c53caa7172802236cd5d60140d7cddd"}, + {file = "grpcio-1.32.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:182c64ade34c341398bf71ec0975613970feb175090760ab4f51d1e9a5424f05"}, + {file = "grpcio-1.32.0-cp35-cp35m-macosx_10_7_intel.whl", hash = "sha256:9c0d8f2346c842088b8cbe3e14985b36e5191a34bf79279ba321a4bf69bd88b7"}, + {file = "grpcio-1.32.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4775bc35af9cd3b5033700388deac2e1d611fa45f4a8dcb93667d94cb25f0444"}, + {file = "grpcio-1.32.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:be98e3198ec765d0a1e27f69d760f69374ded8a33b953dcfe790127731f7e690"}, + {file = "grpcio-1.32.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:378fe80ec5d9353548eb2a8a43ea03747a80f2e387c4f177f2b3ff6c7d898753"}, + {file = "grpcio-1.32.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:f7d508691301027033215d3662dab7e178f54d5cca2329f26a71ae175d94b83f"}, + {file = "grpcio-1.32.0-cp35-cp35m-win32.whl", hash = "sha256:25959a651420dd4a6fd7d3e8dee53f4f5fd8c56336a64963428e78b276389a59"}, + {file = "grpcio-1.32.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ac7028d363d2395f3d755166d0161556a3f99500a5b44890421ccfaaf2aaeb08"}, + {file = "grpcio-1.32.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:c31e8a219650ddae1cd02f5a169e1bffe66a429a8255d3ab29e9363c73003b62"}, + {file = "grpcio-1.32.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e28e4c0d4231beda5dee94808e3a224d85cbaba3cfad05f2192e6f4ec5318053"}, + {file = "grpcio-1.32.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f03dfefa9075dd1c6c5cc27b1285c521434643b09338d8b29e1d6a27b386aa82"}, + {file = "grpcio-1.32.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c4966d746dccb639ef93f13560acbe9630681c07f2b320b7ec03fe2c8f0a1f15"}, + {file = "grpcio-1.32.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:ec10d5f680b8e95a06f1367d73c5ddcc0ed04a3f38d6e4c9346988fb0cea2ffa"}, + {file = "grpcio-1.32.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:28677f057e2ef11501860a7bc15de12091d40b95dd0fddab3c37ff1542e6b216"}, + {file = "grpcio-1.32.0-cp36-cp36m-win32.whl", hash = "sha256:0f3f09269ffd3fded430cd89ba2397eabbf7e47be93983b25c187cdfebb302a7"}, + {file = "grpcio-1.32.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4396b1d0f388ae875eaf6dc05cdcb612c950fd9355bc34d38b90aaa0665a0d4b"}, + {file = "grpcio-1.32.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1ada89326a364a299527c7962e5c362dbae58c67b283fe8383c4d952b26565d5"}, + {file = "grpcio-1.32.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:1d384a61f96a1fc6d5d3e0b62b0a859abc8d4c3f6d16daba51ebf253a3e7df5d"}, + {file = "grpcio-1.32.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e811ce5c387256609d56559d944a974cc6934a8eea8c76e7c86ec388dc06192d"}, + {file = "grpcio-1.32.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:07b430fa68e5eecd78e2ad529ab80f6a234b55fc1b675fe47335ccbf64c6c6c8"}, + {file = "grpcio-1.32.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:0e3edd8cdb71809d2455b9dbff66b4dd3d36c321e64bfa047da5afdfb0db332b"}, + {file = "grpcio-1.32.0-cp37-cp37m-win32.whl", hash = "sha256:6f7947dad606c509d067e5b91a92b250aa0530162ab99e4737090f6b17eb12c4"}, + {file = "grpcio-1.32.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7cda998b7b551503beefc38db9be18c878cfb1596e1418647687575cdefa9273"}, + {file = "grpcio-1.32.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c58825a3d8634cd634d8f869afddd4d5742bdb59d594aea4cea17b8f39269a55"}, + {file = "grpcio-1.32.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ef9bd7fdfc0a063b4ed0efcab7906df5cae9bbcf79d05c583daa2eba56752b00"}, + {file = "grpcio-1.32.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1ce6f5ff4f4a548c502d5237a071fa617115df58ea4b7bd41dac77c1ab126e9c"}, + {file = "grpcio-1.32.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f12900be4c3fd2145ba94ab0d80b7c3d71c9e6414cfee2f31b1c20188b5c281f"}, + {file = "grpcio-1.32.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:f53f2dfc8ff9a58a993e414a016c8b21af333955ae83960454ad91798d467c7b"}, + {file = "grpcio-1.32.0-cp38-cp38-win32.whl", hash = "sha256:5bddf9d53c8df70061916c3bfd2f468ccf26c348bb0fb6211531d895ed5e4c72"}, + {file = "grpcio-1.32.0-cp38-cp38-win_amd64.whl", hash = "sha256:14c0f017bfebbc18139551111ac58ecbde11f4bc375b73a53af38927d60308b6"}, + {file = "grpcio-1.32.0.tar.gz", hash = "sha256:01d3046fe980be25796d368f8fc5ff34b7cf5e1444f3789a017a7fe794465639"}, ] grpcio-tools = [ - {file = "grpcio-tools-1.31.0.tar.gz", hash = "sha256:3b08cbd3f4d5b60e3bff8f859e6e03db739967a684268164abc940415e23ca51"}, - {file = "grpcio_tools-1.31.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:fa472b13319bc68dcb23781682552b3148f09eae5f170ab9c5936bdc43bc154f"}, - {file = "grpcio_tools-1.31.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e687db983f787079cc15055ae4011b8d780568133aec8b8514c2321a48a83c65"}, - {file = "grpcio_tools-1.31.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:82ea52b677477df5d69e0c492c13c75946ef07412101367c2c630f8b08cd3425"}, - {file = "grpcio_tools-1.31.0-cp27-cp27m-win32.whl", hash = "sha256:7ff04f2ef10d0967a990573cec88acc75a0f544d758bd46328f1065bf3f52002"}, - {file = "grpcio_tools-1.31.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e1b46e3812591e0c4c61bbc1c96d92b554a7c0264667349b7ad7a2e2be760fd0"}, - {file = "grpcio_tools-1.31.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:10ab271fd8808fa10f2c9541b3b256c7c678224d3c15f656442709dd9b591082"}, - {file = "grpcio_tools-1.31.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:04320a95426f2a1b0a4b76a98fb79702d0557c9df400e78a9261a38f09475362"}, - {file = "grpcio_tools-1.31.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:150d6e5642211df3e6af56fed963d625ed5e33b7c27d7dee6c400dc8f4edc19e"}, - {file = "grpcio_tools-1.31.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:eab1fd5f65b88ee34f58b823110012d66961b63827b2cdb20fe05df0a2525e5b"}, - {file = "grpcio_tools-1.31.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:7f6edce2d18bccc3a494785519a62bed271c69186a2e8642dc4165066e196d2b"}, - {file = "grpcio_tools-1.31.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:013c11c8be408abcc4fea39a7222d94b84618f0794d3b6db2060ac967042ced2"}, - {file = "grpcio_tools-1.31.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e743797961185266e303cc0c0a365800108184425f31a4182f4ddb9788b59d7"}, - {file = "grpcio_tools-1.31.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:a945723b2205dd542ea9019c61682c50f82559c6f231ad11be848d00295870f1"}, - {file = "grpcio_tools-1.31.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:8faad8223a4c6b351943a0460822a09b7891cd3cdc16b5d3ad3db7516e3f09b2"}, - {file = "grpcio_tools-1.31.0-cp35-cp35m-win32.whl", hash = "sha256:262f53786148e5ba6990cdd91db76a252fef885a34fef94cd0f629103791640f"}, - {file = "grpcio_tools-1.31.0-cp35-cp35m-win_amd64.whl", hash = "sha256:a3a2039d2ee2cb58577dbdef8d70bbfaea03c3f08f8c49b63f854320da0130f9"}, - {file = "grpcio_tools-1.31.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:11849b3ca46d4e4e358d6fbccface509b885a32b91fbe5e9509136e54f2dcd9a"}, - {file = "grpcio_tools-1.31.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cdb52b6340d8e8954e08e5e345cc6b4defa9e0e075b4a57f2053ecac3065037c"}, - {file = "grpcio_tools-1.31.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d36cb70313ab24eae7ca4da611303a768a4ec6739b20c677a39cba43d6a2b70e"}, - {file = "grpcio_tools-1.31.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e2a7ef1deb955d7ea4e0aa8139ad42467710137421001c118e6a15c29662d34c"}, - {file = "grpcio_tools-1.31.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1e454c1e28196d625ba8768baadece37667669a3142ec616245ac119c41b16b5"}, - {file = "grpcio_tools-1.31.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:fe8f8a188ab7e5955126588ba340f9a118d5a61173af8ca1b1cf05544a66312f"}, - {file = "grpcio_tools-1.31.0-cp36-cp36m-win32.whl", hash = "sha256:01efc0467bb67c395ddfe6b27902316ec953c2cd1b4936569665b0a5bccec66e"}, - {file = "grpcio_tools-1.31.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a8d9f98a370332633ebc0c9be769a99b3ca2253dc2cd1ca942c26869fe4207d3"}, - {file = "grpcio_tools-1.31.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da61a10c60685979468ce97dc348ce4ebfc3fa5687874f36c9f834a8e645e9f2"}, - {file = "grpcio_tools-1.31.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8efb0106bc71f984908b8510ffaba8b0a0905b871eb5cc6a684ce7d2e74bbd21"}, - {file = "grpcio_tools-1.31.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8253aa47e1609dcb63001115416c81eada630f2606baace658eb44a36fa570f1"}, - {file = "grpcio_tools-1.31.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:7b1b8943b71709fbd2fc44bc3f5734fccbbd4eb1b13f3f294590dfb9e5ac714d"}, - {file = "grpcio_tools-1.31.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:9d96053c153673ddfa445890f4b1f9ca1b6100bd0f54a80bed29e8c0908cd830"}, - {file = "grpcio_tools-1.31.0-cp37-cp37m-win32.whl", hash = "sha256:d72f1cb2c8d0f20a8356f800c81413798caf29713a03ff8d73c776ba2baca626"}, - {file = "grpcio_tools-1.31.0-cp37-cp37m-win_amd64.whl", hash = "sha256:286979d3780f85ccc80c1dae86e0b649f25f4d18dcae123b87555468b4a14071"}, - {file = "grpcio_tools-1.31.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:66ce45b67b302662b378151522b9cdf04968bbd91678f88982c7b0a1f185fc78"}, - {file = "grpcio_tools-1.31.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:146a583c395b1548fae2162c671c21a0ec1bf628c9d02951f77f28e70eeb7f74"}, - {file = "grpcio_tools-1.31.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5585da595d8d164c2908bffaf39801674974ac95ee47971df87b2c41b36e4c60"}, - {file = "grpcio_tools-1.31.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:a642a0aa02223375c502e8380bb59f9e7bd21084ff7d0380a5b3e16408a6690a"}, - {file = "grpcio_tools-1.31.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:0cde7d7271f2633e7b1cff9bac171921133618ef3452a1e6f1752c0a32dbf94a"}, - {file = "grpcio_tools-1.31.0-cp38-cp38-win32.whl", hash = "sha256:54285a3f9b110b1e08dcf96832f2b9f7d4c4a14556a8bbce41d98cea7f226e6c"}, - {file = "grpcio_tools-1.31.0-cp38-cp38-win_amd64.whl", hash = "sha256:66c4948babbd698fef3e5fa47811267dad08f9cb3455a4866b98c854a4a01f46"}, + {file = "grpcio-tools-1.32.0.tar.gz", hash = "sha256:28547272c51e1d2d343685b9f531e85bb90ad7bd93e726ba646b5627173cbc47"}, + {file = "grpcio_tools-1.32.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6165dc7d424c3c58a54e9e47eacc7cc1513cd09c7c71ff5323e74ead5bb863f"}, + {file = "grpcio_tools-1.32.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a7432b84d6f2f6260d5461eb2a8904db8cf24b663e0a1236375098c8e15c289c"}, + {file = "grpcio_tools-1.32.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4f61edfb0c07689a2835f15f4a25a781f058866cb4fea0bea391ae6deb74325f"}, + {file = "grpcio_tools-1.32.0-cp27-cp27m-win32.whl", hash = "sha256:a3524be59d4e6f8b089f7eaa128bc83e2375aac973f1bf0b568cd1c04c4df56e"}, + {file = "grpcio_tools-1.32.0-cp27-cp27m-win_amd64.whl", hash = "sha256:b31e7e909ba9efd8a08eb45665bf2f8326726da288d9e33555473e6b20596dbd"}, + {file = "grpcio_tools-1.32.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:4e04d6a7c48adbdca64e9b67cc75e8294b3b37b1284dd2819183e38a4207aa39"}, + {file = "grpcio_tools-1.32.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:37acc75ec1dc836772496ef77170fab585e2517abdf1330c29e682eb50a6ce86"}, + {file = "grpcio_tools-1.32.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:7d5be0d06bf830efbf1867db7b01720e54a136454410270e896441ec56baba00"}, + {file = "grpcio_tools-1.32.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:6e26e8d0ef73c04dc1118513c06ff56bce36672c8e28410ae4f938c22002ba00"}, + {file = "grpcio_tools-1.32.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:3971dee0cf57dc3813f6f40724161341ec3b31137b026ae8d4db30c83afeb2a1"}, + {file = "grpcio_tools-1.32.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83414dd919b692d92876db787b6fda709c226243c9bdb71b5025297a127f3be4"}, + {file = "grpcio_tools-1.32.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9b5beb49002bb1f1c0641b55ddc2d1d92c7844fb42348e874146bf7667b6ca20"}, + {file = "grpcio_tools-1.32.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:7a18d6375efe075cc274fdfe004bee4530319a2dbb044eb7eb157c313fe88c97"}, + {file = "grpcio_tools-1.32.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:8147085f0a044ddc27c870feb8e82a25685f3fdf09184dba0f63fed720f12e93"}, + {file = "grpcio_tools-1.32.0-cp35-cp35m-win32.whl", hash = "sha256:e2a37e716ef6b5e81c44648648aae258b67b9ef19e0a472ec4080f5e384be386"}, + {file = "grpcio_tools-1.32.0-cp35-cp35m-win_amd64.whl", hash = "sha256:130c248d0d94473f3eb80d86bdae35a39eb20ab98fde6d227e7f7e053ccbba88"}, + {file = "grpcio_tools-1.32.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:11228fb5343c197e1f4376a966f6845ea270c794ec925260b8a27f6df5d90d04"}, + {file = "grpcio_tools-1.32.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:246caf8cdea97ff3710a810c55c9400e3aa7af1a5464a667d62184e38a58a031"}, + {file = "grpcio_tools-1.32.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:6aa6dd1d7e746c41803a209565d23e6027b0a5dd9b59596da37f99257cc58e65"}, + {file = "grpcio_tools-1.32.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:541a6b992aa417a6305c965bb6896aa1a1ca37d00a82d5438074b18db6a37aad"}, + {file = "grpcio_tools-1.32.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:e83146ef8f17e3a35fe77a438794f0a4a50ea11085194bfea1b419c1b342f7b1"}, + {file = "grpcio_tools-1.32.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:71c451240e66245125e504abee5acc7ab30da099d5c17596d43ecc66e6034e20"}, + {file = "grpcio_tools-1.32.0-cp36-cp36m-win32.whl", hash = "sha256:6155ed6fed3c9a41fd03156c31adb5012c2399992c929987d3fa8ff1cd3c7cd8"}, + {file = "grpcio_tools-1.32.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a137b6079c96f11f0854a4793910f76aa4a62283947311b6e5131369fa226b48"}, + {file = "grpcio_tools-1.32.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f5f381943081792d82fe34c5a649d98a6b91741c6d62cbca8914943b8d1a4e8b"}, + {file = "grpcio_tools-1.32.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:adeae62f3bd1c6839e3822620f7650d30adb7398170e3a0b45a0059f9fe631c8"}, + {file = "grpcio_tools-1.32.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bcc62cb4a3c9a39fb9e349124018e7d7edf0f627592561410e28b590767b831f"}, + {file = "grpcio_tools-1.32.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d11f432ed6fde059b33c514b64fcbf4527f56e03ff94f52f95121547c6945825"}, + {file = "grpcio_tools-1.32.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:708077380f458ef831e7da67f574abfb2fc6b6a24225c5976d92809b8930254c"}, + {file = "grpcio_tools-1.32.0-cp37-cp37m-win32.whl", hash = "sha256:de8ca90742bd41a19c1067fba6ffa13befd3ddb505d67eb297d6a418a5937a25"}, + {file = "grpcio_tools-1.32.0-cp37-cp37m-win_amd64.whl", hash = "sha256:632bba5853e955072392aac42fbca16daf65adfc0ec094fa840afbb83c78bee8"}, + {file = "grpcio_tools-1.32.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2da2a4b2209156d0f88f91bd5d4650a9ed830acb6f685881a26d67d3f671361"}, + {file = "grpcio_tools-1.32.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d3d01ebc1526cc9cdc5e29d2196bae43d56d8ec545dd30fead8b8b3e0b126808"}, + {file = "grpcio_tools-1.32.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fd059d37d9537fa1a89b1139f8cbed7530a5f81c8577560d3f7710fcec95efde"}, + {file = "grpcio_tools-1.32.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:524f0460a49a3248d1cb462d0904e783a75bb3cecdcaea520c3688c8bccd9f2f"}, + {file = "grpcio_tools-1.32.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6327f2c6acca4eac1d5a8e1ee92282682b83069d53199ff8ce18906e912086ed"}, + {file = "grpcio_tools-1.32.0-cp38-cp38-win32.whl", hash = "sha256:07c1da5f1dbd4db664d416f68db6a92d5c88b4073ec6be41fcc7aa4d632f60a9"}, + {file = "grpcio_tools-1.32.0-cp38-cp38-win_amd64.whl", hash = "sha256:9b92f998ed1d01925160e47e9546c742aa0de49009f8fa3bb79420252d8a888d"}, ] grpclib = [ - {file = "grpclib-0.3.2.tar.gz", hash = "sha256:d1e76c56f5b9cf268942b93d1ef2046e3983c35ed3e6b592f02f1d9f0db36a81"}, + {file = "grpclib-0.4.1.tar.gz", hash = "sha256:8c0021cd038634c268249e4cd168d9f3570e66ceceec1c9416094b788ebc8372"}, ] h2 = [ {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"}, @@ -886,6 +1085,10 @@ idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] importlib-metadata = [ {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, @@ -981,8 +1184,8 @@ packaging = [ {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] pastel = [ - {file = "pastel-0.2.0-py2.py3-none-any.whl", hash = "sha256:18b559dc3ad4ba9b8bd5baebe6503f25f36d21460f021cf27a8d889cb5d17840"}, - {file = "pastel-0.2.0.tar.gz", hash = "sha256:46155fc523bdd4efcd450bbcb3f2b94a6e3b25edc0eb493e081104ad09e1ca36"}, + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, ] pathspec = [ {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, @@ -1021,8 +1224,8 @@ py = [ {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, ] pygments = [ - {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, - {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, + {file = "Pygments-2.7.1-py3-none-any.whl", hash = "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998"}, + {file = "Pygments-2.7.1.tar.gz", hash = "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -1043,6 +1246,10 @@ pytest-mock = [ {file = "pytest-mock-3.3.1.tar.gz", hash = "sha256:a4d6d37329e4a893e77d9ffa89e838dd2b45d5dc099984cf03c703ac8411bb82"}, {file = "pytest_mock-3.3.1-py3-none-any.whl", hash = "sha256:024e405ad382646318c4281948aadf6fe1135632bea9cc67366ea0c4098ef5f2"}, ] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] regex = [ {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, @@ -1061,6 +1268,7 @@ regex = [ {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, @@ -1073,13 +1281,49 @@ six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, +] +sphinx = [ + {file = "Sphinx-3.1.2-py3-none-any.whl", hash = "sha256:97dbf2e31fc5684bb805104b8ad34434ed70e6c588f6896991b2fdfd2bef8c00"}, + {file = "Sphinx-3.1.2.tar.gz", hash = "sha256:b9daeb9b39aa1ffefc2809b43604109825300300b987a24f45976c001ba1a8fd"}, +] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-0.5.0-py2.py3-none-any.whl", hash = "sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82"}, + {file = "sphinx_rtd_theme-0.5.0.tar.gz", hash = "sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, + {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, + {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, +] toml = [ {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, ] tox = [ - {file = "tox-3.19.0-py2.py3-none-any.whl", hash = "sha256:3d94b6921a0b6dc90fd8128df83741f30bb41ccd6cd52d131a6a6944ca8f16e6"}, - {file = "tox-3.19.0.tar.gz", hash = "sha256:17e61a93afe5c49281fb969ab71f7a3f22d7586d1c56f9a74219910f356fe7d3"}, + {file = "tox-3.20.0-py2.py3-none-any.whl", hash = "sha256:e6318f404aff16522ff5211c88cab82b39af121735a443674e4e2e65f4e4637b"}, + {file = "tox-3.20.0.tar.gz", hash = "sha256:eb629ddc60e8542fd4a1956b2462e3b8771d49f1ff630cecceacaa0fbfb7605a"}, ] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, @@ -1122,6 +1366,6 @@ wcwidth = [ {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] zipp = [ - {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, + {file = "zipp-3.2.0-py3-none-any.whl", hash = "sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6"}, + {file = "zipp-3.2.0.tar.gz", hash = "sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"}, ] diff --git a/pyproject.toml b/pyproject.toml index 5d609e3ce..b5be6bdd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ python = "^3.6" backports-datetime-fromisoformat = { version = "^1.0.0", python = "<3.7" } black = { version = ">=19.3b0", optional = true } dataclasses = { version = "^0.7", python = ">=3.6, <3.7" } -grpclib = "^0.3.1" +grpclib = "^0.4.1" jinja2 = { version = "^2.11.2", optional = true } protobuf = { version = "^3.12.2", optional = true } @@ -33,6 +33,9 @@ pytest-asyncio = "^0.12.0" pytest-cov = "^2.9.0" pytest-mock = "^3.1.1" tox = "^3.15.1" +sphinx = "3.1.2" +sphinx-rtd-theme = "0.5.0" +asv = "^0.4.2" [tool.poetry.scripts] protoc-gen-python_betterproto = "betterproto.plugin:main" @@ -47,6 +50,8 @@ test = { cmd = "pytest --cov src", help = "Run tests" } types = { cmd = "mypy src --ignore-missing-imports", help = "Check types with mypy" } format = { cmd = "black . --exclude tests/output_", help = "Apply black formatting to source code" } clean = { cmd = "rm -rf .coverage .mypy_cache .pytest_cache dist betterproto.egg-info **/__pycache__ tests/output_*", help = "Clean out generated files from the workspace" } +docs = { cmd = "sphinx-build docs docs/build", help = "Build the sphinx docs"} +bench = { shell = "asv run master^! && asv run HEAD^! && asv compare master HEAD", help = "Benchmark current commit vs. master branch"} # CI tasks full-test = { shell = "poe generate && tox", help = "Run tests with multiple pythons" } @@ -72,5 +77,5 @@ commands = """ [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.0.0,<2"] +build-backend = "poetry.core.masonry.api" diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index 598579878..382a80dc1 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -1,11 +1,10 @@ import dataclasses import enum -import inspect import json import struct import sys import typing -from abc import ABC +from abc import ABCMeta from base64 import b64decode, b64encode from datetime import datetime, timedelta, timezone from typing import ( @@ -14,6 +13,7 @@ Dict, Generator, List, + Mapping, Optional, Set, Tuple, @@ -26,7 +26,7 @@ from .casing import camel_case, safe_snake_case, snake_case from .grpc.grpclib_client import ServiceStub -if not (sys.version_info.major == 3 and sys.version_info.minor >= 7): +if sys.version_info[:2] < (3, 7): # Apply backport of datetime.fromisoformat from 3.7 from backports.datetime_fromisoformat import MonkeyPatch @@ -110,7 +110,7 @@ # Protobuf datetimes start at the Unix Epoch in 1970 in UTC. -def datetime_default_gen(): +def datetime_default_gen() -> datetime: return datetime(1970, 1, 1, tzinfo=timezone.utc) @@ -120,8 +120,8 @@ def datetime_default_gen(): class Casing(enum.Enum): """Casing constants for serialization.""" - CAMEL = camel_case - SNAKE = snake_case + CAMEL = camel_case #: A camelCase sterilization function. + SNAKE = snake_case #: A snake_case sterilization function. PLACEHOLDER: Any = object() @@ -249,11 +249,25 @@ def map_field( class Enum(enum.IntEnum): - """Protocol buffers enumeration base class. Acts like `enum.IntEnum`.""" + """ + The base class for protobuf enumerations, all generated enumerations will inherit + from this. Bases :class:`enum.IntEnum`. + """ @classmethod - def from_string(cls, name: str) -> int: - """Return the value which corresponds to the string name.""" + def from_string(cls, name: str) -> "Enum": + """Return the value which corresponds to the string name. + + Parameters + ----------- + name: :class:`str` + The name of the enum member to get + + Raises + ------- + :exc:`ValueError` + The member was not found in the Enum. + """ try: return cls._member_map_[name] except KeyError as e: @@ -301,11 +315,7 @@ def _preprocess_single(proto_type: str, wraps: str, value: Any) -> bytes: return encode_varint(value) elif proto_type in [TYPE_SINT32, TYPE_SINT64]: # Handle zig-zag encoding. - if value >= 0: - value = value << 1 - else: - value = (value << 1) ^ (~0) - return encode_varint(value) + return encode_varint(value << 1 if value >= 0 else (value << 1) ^ (~0)) elif proto_type in FIXED_TYPES: return struct.pack(_pack_fmt(proto_type), value) elif proto_type == TYPE_STRING: @@ -398,15 +408,15 @@ def parse_fields(value: bytes) -> Generator[ParsedField, None, None]: wire_type = num_wire & 0x7 decoded: Any = None - if wire_type == 0: + if wire_type == WIRE_VARINT: decoded, i = decode_varint(value, i) - elif wire_type == 1: + elif wire_type == WIRE_FIXED_64: decoded, i = value[i : i + 8], i + 8 - elif wire_type == 2: + elif wire_type == WIRE_LEN_DELIM: length, i = decode_varint(value, i) decoded = value[i : i + length] i += length - elif wire_type == 5: + elif wire_type == WIRE_FIXED_32: decoded, i = value[i : i + 4], i + 4 yield ParsedField( @@ -415,12 +425,6 @@ def parse_fields(value: bytes) -> Generator[ParsedField, None, None]: class ProtoClassMetadata: - oneof_group_by_field: Dict[str, str] - oneof_field_by_group: Dict[str, Set[dataclasses.Field]] - default_gen: Dict[str, Callable] - cls_by_field: Dict[str, Type] - field_name_by_number: Dict[int, str] - meta_by_field_name: Dict[str, FieldMetadata] __slots__ = ( "oneof_group_by_field", "oneof_field_by_group", @@ -431,6 +435,14 @@ class ProtoClassMetadata: "sorted_field_names", ) + oneof_group_by_field: Dict[str, str] + oneof_field_by_group: Dict[str, Set[dataclasses.Field]] + field_name_by_number: Dict[int, str] + meta_by_field_name: Dict[str, FieldMetadata] + sorted_field_names: Tuple[str, ...] + default_gen: Dict[str, Callable[[], Any]] + cls_by_field: Dict[str, Type] + def __init__(self, cls: Type["Message"]): by_field = {} by_group: Dict[str, Set] = {} @@ -455,23 +467,21 @@ def __init__(self, cls: Type["Message"]): self.field_name_by_number = by_field_number self.meta_by_field_name = by_field_name self.sorted_field_names = tuple( - by_field_number[number] for number in sorted(by_field_number.keys()) + by_field_number[number] for number in sorted(by_field_number) ) - self.default_gen = self._get_default_gen(cls, fields) self.cls_by_field = self._get_cls_by_field(cls, fields) @staticmethod - def _get_default_gen(cls, fields): - default_gen = {} - - for field in fields: - default_gen[field.name] = cls._get_field_default_gen(field) - - return default_gen + def _get_default_gen( + cls: Type["Message"], fields: List[dataclasses.Field] + ) -> Dict[str, Callable[[], Any]]: + return {field.name: cls._get_field_default_gen(field) for field in fields} @staticmethod - def _get_cls_by_field(cls, fields): + def _get_cls_by_field( + cls: Type["Message"], fields: List[dataclasses.Field] + ) -> Dict[str, Type]: field_cls = {} for field in fields: @@ -480,55 +490,103 @@ def _get_cls_by_field(cls, fields): assert meta.map_types kt = cls._cls_for(field, index=0) vt = cls._cls_for(field, index=1) - field_cls[field.name] = dataclasses.make_dataclass( + field_cls[field.name] = MessageMeta( "Entry", - [ - ("key", kt, dataclass_field(1, meta.map_types[0])), - ("value", vt, dataclass_field(2, meta.map_types[1])), - ], - bases=(Message,), + (Message,), + { + "__annotations__": { + "key": kt, + "value": vt, + }, + "key": dataclass_field(1, meta.map_types[0]), + "value": dataclass_field(2, meta.map_types[1]), + }, ) - field_cls[field.name + ".value"] = vt + field_cls[f"{field.name}.value"] = vt else: field_cls[field.name] = cls._cls_for(field) return field_cls -class Message(ABC): - """ - A protobuf message base class. Generated code will inherit from this and - register the message fields which get used by the serializers and parsers - to go between Python, binary and JSON protobuf message representations. +class MessageMeta(ABCMeta): + """Meta class for all messages. + + Abstracts away any @dataclass decorators with a custom specialized dataclass + implementation for generated messages, mixes in a dataslots implementation as well + for __slot__'ed classes with relatively low overhead. """ - _serialized_on_wire: bool - _unknown_fields: bytes - _group_current: Dict[str, str] + def __new__( + mcs, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any] + ) -> Type["Message"]: + annotations = attrs.get("__annotations__", {}) + attrs["__slots__"] = tuple(annotations) + # Slot the class + + fields = {} + for annotation, type in annotations.items(): + field: Optional[dataclasses.Field] = attrs.pop(annotation, None) + # Remove field class variables from the class namespace + if field is not None: + field.name = annotation + field.type = type + field._field_type = dataclasses._FIELD + fields[annotation] = field + + message_class = super().__new__(mcs, name, bases, attrs) + message_class.__dataclass_fields__ = fields + body = "\n".join( + f" self.{field_name} = {field_name}" for field_name in fields + ) + exec( + f""" +def __init__( + self, {", ".join(f"{field_name} = PLACEHOLDER" for field_name in fields)} +) -> None: + group_current = {'{}'} + object.__setattr__(self, "_unknown_fields", b"") + object.__setattr__(self, "_serialized_on_wire", False) + object.__setattr__(self, "_Message__init_running", True) + for meta in self._betterproto.meta_by_field_name.values(): + if meta.group: + group_current.setdefault(meta.group) + object.__setattr__(self, "_group_current", group_current) +{body} + object.__setattr__(self, "_Message__init_running", False) + self.__post_init__() + +message_class.__init__ = __init__ + """ + ) - def __post_init__(self) -> None: - # Keep track of whether every field was default - all_sentinel = True + return message_class - # Set current field of each group after `__init__` has already been run. - group_current: Dict[str, Optional[str]] = {} - for field_name, meta in self._betterproto.meta_by_field_name.items(): - if meta.group: - group_current.setdefault(meta.group) +class Message(metaclass=MessageMeta): + """ + The base class for protobuf messages, all generated messages will inherit from + this. This class registers the message fields which are used by the serializers and + parsers to go between the Python, binary and JSON representations of the message. + + .. container:: operations + + .. describe:: bytes(x) + + Calls :meth:`__bytes__`. + """ - if self.__raw_get(field_name) != PLACEHOLDER: - # Found a non-sentinel value - all_sentinel = False + _group_current: Dict[str, str] + _serialized_on_wire: bool + _unknown_fields: bytes + __init_running: bool + __dataclass_fields__: Mapping[str, dataclasses.Field] - if meta.group: - # This was set, so make it the selected value of the one-of. - group_current[meta.group] = field_name + def __init__(self, *args: Any, **kwargs: Any) -> None: + ... - # Now that all the defaults are set, reset it! - self.__dict__["_serialized_on_wire"] = not all_sentinel - self.__dict__["_unknown_fields"] = b"" - self.__dict__["_group_current"] = group_current + def __post_init__(self) -> None: + ... def __raw_get(self, name: str) -> Any: return super().__getattribute__(name) @@ -574,24 +632,26 @@ def __getattribute__(self, name: str) -> Any: super().__setattr__(name, value) return value - def __setattr__(self, attr: str, value: Any) -> None: - if attr != "_serialized_on_wire": + def __setattr__(self, name: str, value: Any) -> None: + if self.__init_running and value is PLACEHOLDER: # its a field + return super().__setattr__(name, value) + + if name != "_serialized_on_wire": # Track when a field has been set. - self.__dict__["_serialized_on_wire"] = True - - if hasattr(self, "_group_current"): # __post_init__ had already run - if attr in self._betterproto.oneof_group_by_field: - group = self._betterproto.oneof_group_by_field[attr] - for field in self._betterproto.oneof_field_by_group[group]: - if field.name == attr: - self._group_current[group] = field.name - else: - super().__setattr__(field.name, PLACEHOLDER) + super().__setattr__("_serialized_on_wire", True) + + if name in self._betterproto.oneof_group_by_field: + group = self._betterproto.oneof_group_by_field[name] + for field in self._betterproto.oneof_field_by_group[group]: + if field.name == name: + self._group_current[group] = field.name + else: + super().__setattr__(field.name, PLACEHOLDER) - super().__setattr__(attr, value) + super().__setattr__(name, value) @property - def _betterproto(self): + def _betterproto(self) -> ProtoClassMetadata: """ Lazy initialize metadata for each protobuf class. It may be initialized multiple times in a multi-threaded environment, @@ -605,7 +665,7 @@ def _betterproto(self): def __bytes__(self) -> bytes: """ - Get the binary encoded Protobuf representation of this instance. + Get the binary encoded Protobuf representation of this message instance. """ output = bytearray() for field_name, meta in self._betterproto.meta_by_field_name.items(): @@ -684,7 +744,20 @@ def __bytes__(self) -> bytes: return bytes(output) # For compatibility with other libraries - SerializeToString = __bytes__ + def SerializeToString(self: T) -> bytes: + """ + Get the binary encoded Protobuf representation of this message instance. + + .. note:: + This is a method for compatibility with other libraries, + you should really use ``bytes(x)``. + + Returns + -------- + :class:`bytes` + The binary encoded Protobuf representation of this message instance + """ + return bytes(self) @classmethod def _type_hint(cls, field_name: str) -> Type: @@ -692,9 +765,8 @@ def _type_hint(cls, field_name: str) -> Type: @classmethod def _type_hints(cls) -> Dict[str, Type]: - module = inspect.getmodule(cls) - type_hints = get_type_hints(cls, vars(module)) - return type_hints + module = sys.modules[cls.__module__] + return get_type_hints(cls, vars(module)) @classmethod def _cls_for(cls, field: dataclasses.Field, index: int = 0) -> Type: @@ -705,7 +777,7 @@ def _cls_for(cls, field: dataclasses.Field, index: int = 0) -> Type: field_cls = field_cls.__args__[index] return field_cls - def _get_field_default(self, field_name): + def _get_field_default(self, field_name: str) -> Any: return self._betterproto.default_gen[field_name]() @classmethod @@ -728,7 +800,7 @@ def _get_field_default_gen(cls, field: dataclasses.Field) -> Any: elif issubclass(t, Enum): # Enums always default to zero. return int - elif t == datetime: + elif t is datetime: # Offsets are relative to 1970-01-01T00:00:00Z return datetime_default_gen else: @@ -788,6 +860,16 @@ def parse(self: T, data: bytes) -> T: """ Parse the binary encoded Protobuf into this message instance. This returns the instance itself and is therefore assignable and chainable. + + Parameters + ----------- + data: :class:`bytes` + The data to parse the protobuf from. + + Returns + -------- + :class:`Message` + The initialized message. """ # Got some data over the wire self._serialized_on_wire = True @@ -838,20 +920,47 @@ def parse(self: T, data: bytes) -> T: # For compatibility with other libraries. @classmethod def FromString(cls: Type[T], data: bytes) -> T: + """ + Parse the binary encoded Protobuf into this message instance. This + returns the instance itself and is therefore assignable and chainable. + + .. note:: + This is a method for compatibility with other libraries, + you should really use :meth:`parse`. + + + Parameters + ----------- + data: :class:`bytes` + The data to parse the protobuf from. + + Returns + -------- + :class:`Message` + The initialized message. + """ return cls().parse(data) def to_dict( self, casing: Casing = Casing.CAMEL, include_default_values: bool = False ) -> Dict[str, Any]: """ - Returns a dict representation of this message instance which can be - used to serialize to e.g. JSON. Defaults to camel casing for - compatibility but can be set to other modes. - - `include_default_values` can be set to `True` to include default - values of fields. E.g. an `int32` type field with `0` value will - not be in returned dict if `include_default_values` is set to - `False`. + Returns a JSON serializable dict representation of this object. + + Parameters + ----------- + casing: :class:`Casing` + The casing to use for key values. Default is :attr:`Casing.CAMEL` for + compatibility purposes. + include_default_values: :class:`bool` + If ``True`` will include the default values of fields. Default is ``False``. + E.g. an ``int32`` field will be included with a value of ``0`` if this is + set to ``True``, otherwise this would be ignored. + + Returns + -------- + Dict[:class:`str`, Any] + The JSON serializable dict representation of this object. """ output: Dict[str, Any] = {} field_types = self._type_hints() @@ -895,7 +1004,7 @@ def to_dict( ) ): output[cased_name] = value.to_dict(casing, include_default_values) - elif meta.proto_type == "map": + elif meta.proto_type == TYPE_MAP: for k in value: if hasattr(value[k], "to_dict"): value[k] = value[k].to_dict(casing, include_default_values) @@ -938,10 +1047,20 @@ def to_dict( output[cased_name] = value return output - def from_dict(self: T, value: dict) -> T: + def from_dict(self: T, value: Dict[str, Any]) -> T: """ - Parse the key/value pairs in `value` into this message instance. This - returns the instance itself and is therefore assignable and chainable. + Parse the key/value pairs into the current message instance. This returns the + instance itself and is therefore assignable and chainable. + + Parameters + ----------- + value: Dict[:class:`str`, Any] + The dictionary to parse from. + + Returns + -------- + :class:`Message` + The initialized message. """ self._serialized_on_wire = True for key in value: @@ -951,12 +1070,12 @@ def from_dict(self: T, value: dict) -> T: continue if value[key] is not None: - if meta.proto_type == "message": + if meta.proto_type == TYPE_MESSAGE: v = getattr(self, field_name) if isinstance(v, list): cls = self._betterproto.cls_by_field[field_name] - for i in range(len(value[key])): - v.append(cls().from_dict(value[key][i])) + for item in value[key]: + v.append(cls().from_dict(item)) elif isinstance(v, datetime): v = datetime.fromisoformat(value[key].replace("Z", "+00:00")) setattr(self, field_name, v) @@ -971,7 +1090,7 @@ def from_dict(self: T, value: dict) -> T: v.from_dict(value[key]) elif meta.map_types and meta.map_types[1] == TYPE_MESSAGE: v = getattr(self, field_name) - cls = self._betterproto.cls_by_field[field_name + ".value"] + cls = self._betterproto.cls_by_field[f"{field_name}.value"] for k in value[key]: v[k] = cls().from_dict(value[key][k]) else: @@ -998,51 +1117,96 @@ def from_dict(self: T, value: dict) -> T: return self def to_json(self, indent: Union[None, int, str] = None) -> str: - """Returns the encoded JSON representation of this message instance.""" + """A helper function to parse the message instance into its JSON + representation. + + This is equivalent to:: + + json.dumps(message.to_dict(), indent=indent) + + Parameters + ----------- + indent: Optional[Union[:class:`int`, :class:`str`]] + The indent to pass to :func:`json.dumps`. + + Returns + -------- + :class:`str` + The JSON representation of the message. + """ return json.dumps(self.to_dict(), indent=indent) def from_json(self: T, value: Union[str, bytes]) -> T: - """ - Parse the key/value pairs in `value` into this message instance. This - returns the instance itself and is therefore assignable and chainable. + """A helper function to return the message instance from its JSON + representation. This returns the instance itself and is therefore assignable + and chainable. + + This is equivalent to:: + + return message.from_dict(json.loads(value)) + + Parameters + ----------- + value: Union[:class:`str`, :class:`bytes`] + The value to pass to :func:`json.loads`. + + Returns + -------- + :class:`Message` + The initialized message. """ return self.from_dict(json.loads(value)) def serialized_on_wire(message: Message) -> bool: """ - True if this message was or should be serialized on the wire. This can - be used to detect presence (e.g. optional wrapper message) and is used - internally during parsing/serialization. + If this message was or should be serialized on the wire. This can be used to detect + presence (e.g. optional wrapper message) and is used internally during + parsing/serialization. + + Returns + -------- + :class:`bool` + Whether this message was or should be serialized on the wire. """ return message._serialized_on_wire -def which_one_of(message: Message, group_name: str) -> Tuple[str, Any]: - """Return the name and value of a message's one-of field group.""" +def which_one_of(message: Message, group_name: str) -> Tuple[str, Optional[Any]]: + """ + Return the name and value of a message's one-of field group. + + Returns + -------- + Tuple[:class:`str`, Any] + The field name and the value for that field. + """ field_name = message._group_current.get(group_name) if not field_name: - return ("", None) - return (field_name, getattr(message, field_name)) + return "", None + return field_name, getattr(message, field_name) # Circular import workaround: google.protobuf depends on base classes defined above. from .lib.google.protobuf import ( # noqa - Duration, - Timestamp, BoolValue, BytesValue, DoubleValue, + Duration, FloatValue, Int32Value, Int64Value, StringValue, + Timestamp, UInt32Value, UInt64Value, ) class _Duration(Duration): + seconds: int = int64_field(1) + nanos: int = int32_field(2) + def to_timedelta(self) -> timedelta: return timedelta(seconds=self.seconds, microseconds=self.nanos / 1e3) @@ -1051,11 +1215,14 @@ def delta_to_json(delta: timedelta) -> str: parts = str(delta.total_seconds()).split(".") if len(parts) > 1: while len(parts[1]) not in [3, 6, 9]: - parts[1] = parts[1] + "0" - return ".".join(parts) + "s" + parts[1] = f"{parts[1]}0" + return f"{'.'.join(parts)}s" class _Timestamp(Timestamp): + seconds: int = int64_field(1) + nanos: int = int32_field(2) + def to_datetime(self) -> datetime: ts = self.seconds + (self.nanos / 1e9) return datetime.fromtimestamp(ts, tz=timezone.utc) @@ -1068,15 +1235,15 @@ def timestamp_to_json(dt: datetime) -> str: if (nanos % 1e9) == 0: # If there are 0 fractional digits, the fractional # point '.' should be omitted when serializing. - return result + "Z" + return f"{result}Z" if (nanos % 1e6) == 0: # Serialize 3 fractional digits. - return result + ".%03dZ" % (nanos / 1e6) + return f"{result}.{int(nanos // 1e6) :03d}Z" if (nanos % 1e3) == 0: # Serialize 6 fractional digits. - return result + ".%06dZ" % (nanos / 1e3) + return f"{result}.{int(nanos // 1e3) :06d}Z" # Serialize 9 fractional digits. - return result + ".%09dZ" % nanos + return f"{result}.{nanos:09d}" class _WrappedMessage(Message): diff --git a/src/betterproto/_types.py b/src/betterproto/_types.py index bc3748f8f..26b734406 100644 --- a/src/betterproto/_types.py +++ b/src/betterproto/_types.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING, TypeVar if TYPE_CHECKING: - from . import Message from grpclib._typing import IProtoMessage + from . import Message # Bound type variable to allow methods to return `self` of subclasses T = TypeVar("T", bound="Message") diff --git a/src/betterproto/casing.py b/src/betterproto/casing.py index e21e7fdbb..cd3c34472 100644 --- a/src/betterproto/casing.py +++ b/src/betterproto/casing.py @@ -21,14 +21,24 @@ def safe_snake_case(value: str) -> str: return value -def snake_case(value: str, strict: bool = True): +def snake_case(value: str, strict: bool = True) -> str: """ Join words with an underscore into lowercase and remove symbols. - @param value: value to convert - @param strict: force single underscores + + Parameters + ----------- + value: :class:`str` + The value to convert. + strict: :class:`bool` + Whether or not to force single underscores. + + Returns + -------- + :class:`str` + The value in snake_case. """ - def substitute_word(symbols, word, is_start): + def substitute_word(symbols: str, word: str, is_start: bool) -> str: if not word: return "" if strict: @@ -52,11 +62,21 @@ def substitute_word(symbols, word, is_start): return snake -def pascal_case(value: str, strict: bool = True): +def pascal_case(value: str, strict: bool = True) -> str: """ Capitalize each word and remove symbols. - @param value: value to convert - @param strict: output only alphanumeric characters + + Parameters + ----------- + value: :class:`str` + The value to convert. + strict: :class:`bool` + Whether or not to output only alphanumeric characters. + + Returns + -------- + :class:`str` + The value in PascalCase. """ def substitute_word(symbols, word): @@ -77,14 +97,39 @@ def substitute_word(symbols, word): ) -def camel_case(value: str, strict: bool = True): +def camel_case(value: str, strict: bool = True) -> str: """ Capitalize all words except first and remove symbols. + + Parameters + ----------- + value: :class:`str` + The value to convert. + strict: :class:`bool` + Whether or not to output only alphanumeric characters. + + Returns + -------- + :class:`str` + The value in camelCase. """ return lowercase_first(pascal_case(value, strict=strict)) -def lowercase_first(value: str): +def lowercase_first(value: str) -> str: + """ + Lower cases the first character of the value. + + Parameters + ---------- + value: :class:`str` + The value to lower case. + + Returns + ------- + :class:`str` + The lower cased string. + """ return value[0:1].lower() + value[1:] diff --git a/src/betterproto/compile/importing.py b/src/betterproto/compile/importing.py index 1e245e427..8d471d134 100644 --- a/src/betterproto/compile/importing.py +++ b/src/betterproto/compile/importing.py @@ -1,10 +1,10 @@ import os import re -from typing import Dict, List, Set, Type +from typing import Dict, List, Set, Tuple, Type -from betterproto import safe_snake_case -from betterproto.compile.naming import pythonize_class_name -from betterproto.lib.google import protobuf as google_protobuf +from ..casing import safe_snake_case +from ..lib.google import protobuf as google_protobuf +from .naming import pythonize_class_name WRAPPER_TYPES: Dict[str, Type] = { ".google.protobuf.DoubleValue": google_protobuf.DoubleValue, @@ -19,7 +19,7 @@ } -def parse_source_type_name(field_type_name): +def parse_source_type_name(field_type_name: str) -> Tuple[str, str]: """ Split full source type name into package and type name. E.g. 'root.package.Message' -> ('root.package', 'Message') @@ -50,7 +50,7 @@ def get_type_reference( if source_type == ".google.protobuf.Duration": return "timedelta" - if source_type == ".google.protobuf.Timestamp": + elif source_type == ".google.protobuf.Timestamp": return "datetime" source_package, source_type = parse_source_type_name(source_type) @@ -79,7 +79,7 @@ def get_type_reference( return reference_cousin(current_package, imports, py_package, py_type) -def reference_absolute(imports, py_package, py_type): +def reference_absolute(imports: Set[str], py_package: List[str], py_type: str) -> str: """ Returns a reference to a python type located in the root, i.e. sys.path. """ diff --git a/src/betterproto/compile/naming.py b/src/betterproto/compile/naming.py index 3d5685234..1c2dbabee 100644 --- a/src/betterproto/compile/naming.py +++ b/src/betterproto/compile/naming.py @@ -1,13 +1,13 @@ from betterproto import casing -def pythonize_class_name(name): +def pythonize_class_name(name: str) -> str: return casing.pascal_case(name) -def pythonize_field_name(name: str): +def pythonize_field_name(name: str) -> str: return casing.safe_snake_case(name) -def pythonize_method_name(name: str): +def pythonize_method_name(name: str) -> str: return casing.safe_snake_case(name) diff --git a/src/betterproto/grpc/grpclib_client.py b/src/betterproto/grpc/grpclib_client.py index 6fa35b46f..a22b7e358 100644 --- a/src/betterproto/grpc/grpclib_client.py +++ b/src/betterproto/grpc/grpclib_client.py @@ -1,7 +1,7 @@ -from abc import ABC import asyncio -import grpclib.const +from abc import ABC from typing import ( + TYPE_CHECKING, AsyncIterable, AsyncIterator, Collection, @@ -9,11 +9,13 @@ Mapping, Optional, Tuple, - TYPE_CHECKING, Type, Union, ) -from betterproto._types import ST, T + +import grpclib.const + +from .._types import ST, T if TYPE_CHECKING: from grpclib.client import Channel diff --git a/src/betterproto/grpc/util/async_channel.py b/src/betterproto/grpc/util/async_channel.py index 0cda4b2ff..9b822fe83 100644 --- a/src/betterproto/grpc/util/async_channel.py +++ b/src/betterproto/grpc/util/async_channel.py @@ -1,12 +1,5 @@ import asyncio -from typing import ( - AsyncIterable, - AsyncIterator, - Iterable, - Optional, - TypeVar, - Union, -) +from typing import AsyncIterable, AsyncIterator, Iterable, Optional, TypeVar, Union T = TypeVar("T") @@ -16,8 +9,6 @@ class ChannelClosed(Exception): An exception raised on an attempt to send through a closed channel """ - pass - class ChannelDone(Exception): """ @@ -25,8 +16,6 @@ class ChannelDone(Exception): and empty. """ - pass - class AsyncChannel(AsyncIterable[T]): """ diff --git a/src/betterproto/lib/google/protobuf/__init__.py b/src/betterproto/lib/google/protobuf/__init__.py index 936d17529..5a179b7e8 100644 --- a/src/betterproto/lib/google/protobuf/__init__.py +++ b/src/betterproto/lib/google/protobuf/__init__.py @@ -1,7 +1,6 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # sources: google/protobuf/any.proto, google/protobuf/source_context.proto, google/protobuf/type.proto, google/protobuf/api.proto, google/protobuf/descriptor.proto, google/protobuf/duration.proto, google/protobuf/empty.proto, google/protobuf/field_mask.proto, google/protobuf/struct.proto, google/protobuf/timestamp.proto, google/protobuf/wrappers.proto # plugin: python-betterproto -from dataclasses import dataclass from typing import Dict, List import betterproto @@ -107,7 +106,6 @@ class NullValue(betterproto.Enum): NULL_VALUE = 0 -@dataclass class Any(betterproto.Message): """ `Any` contains an arbitrary serialized protocol buffer message along with a @@ -165,7 +163,6 @@ class Any(betterproto.Message): value: bytes = betterproto.bytes_field(2) -@dataclass class SourceContext(betterproto.Message): """ `SourceContext` represents information about the source of a protobuf @@ -177,7 +174,6 @@ class SourceContext(betterproto.Message): file_name: str = betterproto.string_field(1) -@dataclass class Type(betterproto.Message): """A protocol buffer message type.""" @@ -195,7 +191,6 @@ class Type(betterproto.Message): syntax: "Syntax" = betterproto.enum_field(6) -@dataclass class Field(betterproto.Message): """A single field of a message type.""" @@ -223,7 +218,6 @@ class Field(betterproto.Message): default_value: str = betterproto.string_field(11) -@dataclass class Enum(betterproto.Message): """Enum type definition.""" @@ -241,7 +235,6 @@ class Enum(betterproto.Message): syntax: "Syntax" = betterproto.enum_field(5) -@dataclass class EnumValue(betterproto.Message): """Enum value definition.""" @@ -253,7 +246,6 @@ class EnumValue(betterproto.Message): options: List["Option"] = betterproto.message_field(3) -@dataclass class Option(betterproto.Message): """ A protocol buffer option, which can be attached to a message, field, @@ -272,7 +264,6 @@ class Option(betterproto.Message): value: "Any" = betterproto.message_field(2) -@dataclass class Api(betterproto.Message): """ Api is a light-weight descriptor for an API Interface. Interfaces are also @@ -315,7 +306,6 @@ class Api(betterproto.Message): syntax: "Syntax" = betterproto.enum_field(7) -@dataclass class Method(betterproto.Message): """Method represents a method of an API interface.""" @@ -335,7 +325,6 @@ class Method(betterproto.Message): syntax: "Syntax" = betterproto.enum_field(7) -@dataclass class Mixin(betterproto.Message): """ Declares an API Interface to be included in this interface. The including @@ -380,7 +369,6 @@ class Mixin(betterproto.Message): root: str = betterproto.string_field(2) -@dataclass class FileDescriptorSet(betterproto.Message): """ The protocol compiler can output a FileDescriptorSet containing the .proto @@ -390,7 +378,6 @@ class FileDescriptorSet(betterproto.Message): file: List["FileDescriptorProto"] = betterproto.message_field(1) -@dataclass class FileDescriptorProto(betterproto.Message): """Describes a complete .proto file.""" @@ -419,7 +406,6 @@ class FileDescriptorProto(betterproto.Message): syntax: str = betterproto.string_field(12) -@dataclass class DescriptorProto(betterproto.Message): """Describes a message type.""" @@ -439,14 +425,12 @@ class DescriptorProto(betterproto.Message): reserved_name: List[str] = betterproto.string_field(10) -@dataclass class DescriptorProtoExtensionRange(betterproto.Message): start: int = betterproto.int32_field(1) end: int = betterproto.int32_field(2) options: "ExtensionRangeOptions" = betterproto.message_field(3) -@dataclass class DescriptorProtoReservedRange(betterproto.Message): """ Range of reserved tag numbers. Reserved tag numbers may not be used by @@ -458,13 +442,11 @@ class DescriptorProtoReservedRange(betterproto.Message): end: int = betterproto.int32_field(2) -@dataclass class ExtensionRangeOptions(betterproto.Message): # The parser stores options it doesn't recognize here. See above. uninterpreted_option: List["UninterpretedOption"] = betterproto.message_field(999) -@dataclass class FieldDescriptorProto(betterproto.Message): """Describes a field within a message.""" @@ -498,7 +480,6 @@ class FieldDescriptorProto(betterproto.Message): options: "FieldOptions" = betterproto.message_field(8) -@dataclass class OneofDescriptorProto(betterproto.Message): """Describes a oneof.""" @@ -506,7 +487,6 @@ class OneofDescriptorProto(betterproto.Message): options: "OneofOptions" = betterproto.message_field(2) -@dataclass class EnumDescriptorProto(betterproto.Message): """Describes an enum type.""" @@ -526,7 +506,6 @@ class EnumDescriptorProto(betterproto.Message): reserved_name: List[str] = betterproto.string_field(5) -@dataclass class EnumDescriptorProtoEnumReservedRange(betterproto.Message): """ Range of reserved numeric values. Reserved values may not be used by @@ -539,7 +518,6 @@ class EnumDescriptorProtoEnumReservedRange(betterproto.Message): end: int = betterproto.int32_field(2) -@dataclass class EnumValueDescriptorProto(betterproto.Message): """Describes a value within an enum.""" @@ -550,7 +528,6 @@ class EnumValueDescriptorProto(betterproto.Message): ) -@dataclass class ServiceDescriptorProto(betterproto.Message): """Describes a service.""" @@ -559,7 +536,6 @@ class ServiceDescriptorProto(betterproto.Message): options: "ServiceOptions" = betterproto.message_field(3) -@dataclass class MethodDescriptorProto(betterproto.Message): """Describes a method of a service.""" @@ -575,7 +551,6 @@ class MethodDescriptorProto(betterproto.Message): server_streaming: bool = betterproto.bool_field(6) -@dataclass class FileOptions(betterproto.Message): # Sets the Java package where classes generated from this .proto will be # placed. By default, the proto package is used, but this is often @@ -658,7 +633,6 @@ class FileOptions(betterproto.Message): uninterpreted_option: List["UninterpretedOption"] = betterproto.message_field(999) -@dataclass class MessageOptions(betterproto.Message): # Set true to use the old proto1 MessageSet wire format for extensions. This # is provided for backwards-compatibility with the MessageSet wire format. @@ -695,7 +669,6 @@ class MessageOptions(betterproto.Message): uninterpreted_option: List["UninterpretedOption"] = betterproto.message_field(999) -@dataclass class FieldOptions(betterproto.Message): # The ctype option instructs the C++ code generator to use a different # representation of the field than it normally would. See the specific @@ -752,13 +725,11 @@ class FieldOptions(betterproto.Message): uninterpreted_option: List["UninterpretedOption"] = betterproto.message_field(999) -@dataclass class OneofOptions(betterproto.Message): # The parser stores options it doesn't recognize here. See above. uninterpreted_option: List["UninterpretedOption"] = betterproto.message_field(999) -@dataclass class EnumOptions(betterproto.Message): # Set this option to true to allow mapping different tag names to the same # value. @@ -771,7 +742,6 @@ class EnumOptions(betterproto.Message): uninterpreted_option: List["UninterpretedOption"] = betterproto.message_field(999) -@dataclass class EnumValueOptions(betterproto.Message): # Is this enum value deprecated? Depending on the target platform, this can # emit Deprecated annotations for the enum value, or it will be completely @@ -782,7 +752,6 @@ class EnumValueOptions(betterproto.Message): uninterpreted_option: List["UninterpretedOption"] = betterproto.message_field(999) -@dataclass class ServiceOptions(betterproto.Message): # Is this service deprecated? Depending on the target platform, this can emit # Deprecated annotations for the service, or it will be completely ignored; @@ -792,7 +761,6 @@ class ServiceOptions(betterproto.Message): uninterpreted_option: List["UninterpretedOption"] = betterproto.message_field(999) -@dataclass class MethodOptions(betterproto.Message): # Is this method deprecated? Depending on the target platform, this can emit # Deprecated annotations for the method, or it will be completely ignored; in @@ -803,7 +771,6 @@ class MethodOptions(betterproto.Message): uninterpreted_option: List["UninterpretedOption"] = betterproto.message_field(999) -@dataclass class UninterpretedOption(betterproto.Message): """ A message representing a option the parser does not recognize. This only @@ -825,7 +792,6 @@ class UninterpretedOption(betterproto.Message): aggregate_value: str = betterproto.string_field(8) -@dataclass class UninterpretedOptionNamePart(betterproto.Message): """ The name of the uninterpreted option. Each string represents a segment in @@ -839,7 +805,6 @@ class UninterpretedOptionNamePart(betterproto.Message): is_extension: bool = betterproto.bool_field(2) -@dataclass class SourceCodeInfo(betterproto.Message): """ Encapsulates information about the original source file from which a @@ -878,7 +843,6 @@ class SourceCodeInfo(betterproto.Message): location: List["SourceCodeInfoLocation"] = betterproto.message_field(1) -@dataclass class SourceCodeInfoLocation(betterproto.Message): # Identifies which part of the FileDescriptorProto was defined at this # location. Each element is a field number or an index. They form a path @@ -925,7 +889,6 @@ class SourceCodeInfoLocation(betterproto.Message): leading_detached_comments: List[str] = betterproto.string_field(6) -@dataclass class GeneratedCodeInfo(betterproto.Message): """ Describes the relationship between generated code and its original source @@ -938,7 +901,6 @@ class GeneratedCodeInfo(betterproto.Message): annotation: List["GeneratedCodeInfoAnnotation"] = betterproto.message_field(1) -@dataclass class GeneratedCodeInfoAnnotation(betterproto.Message): # Identifies the element in the original source .proto file. This field is # formatted the same as SourceCodeInfo.Location.path. @@ -954,7 +916,6 @@ class GeneratedCodeInfoAnnotation(betterproto.Message): end: int = betterproto.int32_field(4) -@dataclass class Duration(betterproto.Message): """ A Duration represents a signed, fixed-length span of time represented as a @@ -999,7 +960,6 @@ class Duration(betterproto.Message): nanos: int = betterproto.int32_field(2) -@dataclass class Empty(betterproto.Message): """ A generic empty message that you can re-use to avoid defining duplicated @@ -1012,7 +972,6 @@ class Empty(betterproto.Message): pass -@dataclass class FieldMask(betterproto.Message): """ `FieldMask` represents a set of symbolic field paths, for example: @@ -1096,7 +1055,6 @@ class FieldMask(betterproto.Message): paths: List[str] = betterproto.string_field(1) -@dataclass class Struct(betterproto.Message): """ `Struct` represents a structured data value, consisting of fields which map @@ -1113,7 +1071,6 @@ class Struct(betterproto.Message): ) -@dataclass class Value(betterproto.Message): """ `Value` represents a dynamically typed value which can be either null, a @@ -1137,7 +1094,6 @@ class Value(betterproto.Message): list_value: "ListValue" = betterproto.message_field(6, group="kind") -@dataclass class ListValue(betterproto.Message): """ `ListValue` is a wrapper around a repeated field of values. The JSON @@ -1148,7 +1104,6 @@ class ListValue(betterproto.Message): values: List["Value"] = betterproto.message_field(1) -@dataclass class Timestamp(betterproto.Message): """ A Timestamp represents a point in time independent of any time zone or @@ -1213,7 +1168,6 @@ class Timestamp(betterproto.Message): nanos: int = betterproto.int32_field(2) -@dataclass class DoubleValue(betterproto.Message): """ Wrapper message for `double`. The JSON representation for `DoubleValue` is @@ -1224,7 +1178,6 @@ class DoubleValue(betterproto.Message): value: float = betterproto.double_field(1) -@dataclass class FloatValue(betterproto.Message): """ Wrapper message for `float`. The JSON representation for `FloatValue` is @@ -1235,7 +1188,6 @@ class FloatValue(betterproto.Message): value: float = betterproto.float_field(1) -@dataclass class Int64Value(betterproto.Message): """ Wrapper message for `int64`. The JSON representation for `Int64Value` is @@ -1246,7 +1198,6 @@ class Int64Value(betterproto.Message): value: int = betterproto.int64_field(1) -@dataclass class UInt64Value(betterproto.Message): """ Wrapper message for `uint64`. The JSON representation for `UInt64Value` is @@ -1257,7 +1208,6 @@ class UInt64Value(betterproto.Message): value: int = betterproto.uint64_field(1) -@dataclass class Int32Value(betterproto.Message): """ Wrapper message for `int32`. The JSON representation for `Int32Value` is @@ -1268,7 +1218,6 @@ class Int32Value(betterproto.Message): value: int = betterproto.int32_field(1) -@dataclass class UInt32Value(betterproto.Message): """ Wrapper message for `uint32`. The JSON representation for `UInt32Value` is @@ -1279,7 +1228,6 @@ class UInt32Value(betterproto.Message): value: int = betterproto.uint32_field(1) -@dataclass class BoolValue(betterproto.Message): """ Wrapper message for `bool`. The JSON representation for `BoolValue` is JSON @@ -1290,7 +1238,6 @@ class BoolValue(betterproto.Message): value: bool = betterproto.bool_field(1) -@dataclass class StringValue(betterproto.Message): """ Wrapper message for `string`. The JSON representation for `StringValue` is @@ -1301,7 +1248,6 @@ class StringValue(betterproto.Message): value: str = betterproto.string_field(1) -@dataclass class BytesValue(betterproto.Message): """ Wrapper message for `bytes`. The JSON representation for `BytesValue` is diff --git a/src/betterproto/plugin/compiler.py b/src/betterproto/plugin/compiler.py index 4fd3b8fcf..28cfe3d94 100644 --- a/src/betterproto/plugin/compiler.py +++ b/src/betterproto/plugin/compiler.py @@ -5,10 +5,9 @@ import black import jinja2 except ImportError as err: - missing_import = err.args[0][17:-1] print( "\033[31m" - f"Unable to import `{missing_import}` from betterproto plugin! " + f"Unable to import `{err.name}` from betterproto plugin! " "Please ensure that you've installed betterproto as " '`pip install "betterproto[compiler]"` so that compiler dependencies ' "are included." @@ -16,7 +15,7 @@ ) raise SystemExit(1) -from betterproto.plugin.models import OutputTemplate +from .models import OutputTemplate def outputfile_compiler(output_file: OutputTemplate) -> str: @@ -32,9 +31,7 @@ def outputfile_compiler(output_file: OutputTemplate) -> str: ) template = env.get_template("template.py.j2") - res = black.format_str( + return black.format_str( template.render(output_file=output_file), - mode=black.FileMode(target_versions={black.TargetVersion.PY37}), + mode=black.Mode(target_versions={black.TargetVersion.PY37}), ) - - return res diff --git a/src/betterproto/plugin/main.py b/src/betterproto/plugin/main.py index 2604af2b8..dc9d04c94 100644 --- a/src/betterproto/plugin/main.py +++ b/src/betterproto/plugin/main.py @@ -1,13 +1,14 @@ #!/usr/bin/env python -import sys + import os +import sys from google.protobuf.compiler import plugin_pb2 as plugin from betterproto.plugin.parser import generate_code -def main(): +def main() -> None: """The plugin's main entry point.""" # Read request message from stdin data = sys.stdin.buffer.read() @@ -33,7 +34,7 @@ def main(): sys.stdout.buffer.write(output) -def dump_request(dump_file: str, request: plugin.CodeGeneratorRequest): +def dump_request(dump_file: str, request: plugin.CodeGeneratorRequest) -> None: """ For developers: Supports running plugin.py standalone so its possible to debug it. Run protoc (or generate.py) with BETTERPROTO_DUMP="yourfile.bin" to write the request to a file. diff --git a/src/betterproto/plugin/models.py b/src/betterproto/plugin/models.py index bbd21e670..890cb00dc 100644 --- a/src/betterproto/plugin/models.py +++ b/src/betterproto/plugin/models.py @@ -1,14 +1,14 @@ """Plugin model dataclasses. These classes are meant to be an intermediate representation -of protbuf objects. They are used to organize the data collected during parsing. +of protobuf objects. They are used to organize the data collected during parsing. The general intention is to create a doubly-linked tree-like structure with the following types of references: - Downwards references: from message -> fields, from output package -> messages or from service -> service methods - Upwards references: from field -> message, message -> package. -- Input/ouput message references: from a service method to it's corresponding +- Input/output message references: from a service method to it's corresponding input/output messages, which may even be in another package. There are convenience methods to allow climbing up and down this tree, for @@ -26,36 +26,24 @@ The instantiation should also attach a reference to the new object into the corresponding place within it's parent object. For example, instantiating field `A` with parent message `B` should add a -reference to `A` to `B`'s `fields` attirbute. +reference to `A` to `B`'s `fields` attribute. """ import re -from dataclasses import dataclass -from dataclasses import field -from typing import ( - Iterator, - Union, - Type, - List, - Dict, - Set, - Text, -) import textwrap +from dataclasses import dataclass, field +from typing import Dict, Iterator, List, Optional, Set, Text, Type, Union import betterproto -from betterproto.compile.importing import ( - get_type_reference, - parse_source_type_name, -) -from betterproto.compile.naming import ( + +from ..casing import sanitize_name +from ..compile.importing import get_type_reference, parse_source_type_name +from ..compile.naming import ( pythonize_class_name, pythonize_field_name, pythonize_method_name, ) -from ..casing import sanitize_name - try: # betterproto[compiler] specific dependencies from google.protobuf.compiler import plugin_pb2 as plugin @@ -67,10 +55,9 @@ MethodDescriptorProto, ) except ImportError as err: - missing_import = re.match(r".*(cannot import name .*$)", err.args[0]).group(1) print( "\033[31m" - f"Unable to import `{missing_import}` from betterproto plugin! " + f"Unable to import `{err.name}` from betterproto plugin! " "Please ensure that you've installed betterproto as " '`pip install "betterproto[compiler]"` so that compiler dependencies ' "are included." @@ -124,10 +111,11 @@ ) -def get_comment(proto_file, path: List[int], indent: int = 4) -> str: +def get_comment( + proto_file: "FileDescriptorProto", path: List[int], indent: int = 4 +) -> str: pad = " " * indent for sci in proto_file.source_code_info.location: - # print(list(sci.path), path, file=sys.stderr) if list(sci.path) == path and sci.leading_comments: lines = textwrap.wrap( sci.leading_comments.strip().replace("\n", ""), width=79 - indent @@ -153,9 +141,9 @@ class ProtoContentBase: path: List[int] comment_indent: int = 4 - parent: Union["Messsage", "OutputTemplate"] + parent: Union["betterproto.Message", "OutputTemplate"] - def __post_init__(self): + def __post_init__(self) -> None: """Checks that no fake default fields were left as placeholders.""" for field_name, field_val in self.__dataclass_fields__.items(): if field_val is PLACEHOLDER: @@ -273,7 +261,7 @@ class MessageCompiler(ProtoContentBase): ) deprecated: bool = field(default=False, init=False) - def __post_init__(self): + def __post_init__(self) -> None: # Add message to output file if isinstance(self.parent, OutputTemplate): if isinstance(self, EnumDefinitionCompiler): @@ -303,6 +291,10 @@ def deprecated_fields(self) -> Iterator[str]: if f.deprecated: yield f.py_name + @property + def has_deprecated_fields(self) -> bool: + return any(self.deprecated_fields) + def is_map( proto_field_obj: FieldDescriptorProto, parent_message: DescriptorProto @@ -314,17 +306,17 @@ def is_map( map_entry = f"{proto_field_obj.name.replace('_', '').lower()}entry" if message_type == map_entry: for nested in parent_message.nested_type: # parent message - if nested.name.replace("_", "").lower() == map_entry: - if nested.options.map_entry: - return True + if ( + nested.name.replace("_", "").lower() == map_entry + and nested.options.map_entry + ): + return True return False def is_oneof(proto_field_obj: FieldDescriptorProto) -> bool: """True if proto_field_obj is a OneOf, otherwise False.""" - if proto_field_obj.HasField("oneof_index"): - return True - return False + return proto_field_obj.HasField("oneof_index") @dataclass @@ -332,7 +324,7 @@ class FieldCompiler(MessageCompiler): parent: MessageCompiler = PLACEHOLDER proto_obj: FieldDescriptorProto = PLACEHOLDER - def __post_init__(self): + def __post_init__(self) -> None: # Add field to message self.parent.fields.append(self) # Check for new imports @@ -357,11 +349,9 @@ def get_field_string(self, indent: int = 4) -> str: ([""] + self.betterproto_field_args) if self.betterproto_field_args else [] ) betterproto_field_type = ( - f"betterproto.{self.field_type}_field({self.proto_obj.number}" - + field_args - + ")" + f"betterproto.{self.field_type}_field({self.proto_obj.number}{field_args})" ) - return name + annotations + " = " + betterproto_field_type + return f"{name}{annotations} = {betterproto_field_type}" @property def betterproto_field_args(self) -> List[str]: @@ -371,7 +361,7 @@ def betterproto_field_args(self) -> List[str]: return args @property - def field_wraps(self) -> Union[str, None]: + def field_wraps(self) -> Optional[str]: """Returns betterproto wrapped field type or None.""" match_wrapper = re.match( r"\.google\.protobuf\.(.+)Value", self.proto_obj.type_name @@ -384,17 +374,15 @@ def field_wraps(self) -> Union[str, None]: @property def repeated(self) -> bool: - if self.proto_obj.label == FieldDescriptorProto.LABEL_REPEATED and not is_map( - self.proto_obj, self.parent - ): - return True - return False + return ( + self.proto_obj.label == FieldDescriptorProto.LABEL_REPEATED + and not is_map(self.proto_obj, self.parent) + ) @property def mutable(self) -> bool: """True if the field is a mutable type, otherwise False.""" - annotation = self.annotation - return annotation.startswith("List[") or annotation.startswith("Dict[") + return self.annotation.startswith(("List[", "Dict[")) @property def field_type(self) -> str: @@ -425,9 +413,7 @@ def default_value_string(self) -> Union[Text, None, float, int]: @property def packed(self) -> bool: """True if the wire representation is a packed format.""" - if self.repeated and self.proto_obj.type in PROTO_PACKED_TYPES: - return True - return False + return self.repeated and self.proto_obj.type in PROTO_PACKED_TYPES @property def py_name(self) -> str: @@ -486,22 +472,24 @@ class MapEntryCompiler(FieldCompiler): proto_k_type: str = PLACEHOLDER proto_v_type: str = PLACEHOLDER - def __post_init__(self): + def __post_init__(self) -> None: """Explore nested types and set k_type and v_type if unset.""" map_entry = f"{self.proto_obj.name.replace('_', '').lower()}entry" for nested in self.parent.proto_obj.nested_type: - if nested.name.replace("_", "").lower() == map_entry: - if nested.options.map_entry: - # Get Python types - self.py_k_type = FieldCompiler( - parent=self, proto_obj=nested.field[0] # key - ).py_type - self.py_v_type = FieldCompiler( - parent=self, proto_obj=nested.field[1] # value - ).py_type - # Get proto types - self.proto_k_type = self.proto_obj.Type.Name(nested.field[0].type) - self.proto_v_type = self.proto_obj.Type.Name(nested.field[1].type) + if ( + nested.name.replace("_", "").lower() == map_entry + and nested.options.map_entry + ): + # Get Python types + self.py_k_type = FieldCompiler( + parent=self, proto_obj=nested.field[0] # key + ).py_type + self.py_v_type = FieldCompiler( + parent=self, proto_obj=nested.field[1] # value + ).py_type + # Get proto types + self.proto_k_type = self.proto_obj.Type.Name(nested.field[0].type) + self.proto_v_type = self.proto_obj.Type.Name(nested.field[1].type) super().__post_init__() # call FieldCompiler-> MessageCompiler __post_init__ @property @@ -513,11 +501,11 @@ def field_type(self) -> str: return "map" @property - def annotation(self): + def annotation(self) -> str: return f"Dict[{self.py_k_type}, {self.py_v_type}]" @property - def repeated(self): + def repeated(self) -> bool: return False # maps cannot be repeated @@ -536,7 +524,7 @@ class EnumEntry: value: int comment: str - def __post_init__(self): + def __post_init__(self) -> None: # Get entries/allowed values for this Enum self.entries = [ self.EnumEntry( @@ -551,7 +539,7 @@ def __post_init__(self): super().__post_init__() # call MessageCompiler __post_init__ @property - def default_value_string(self) -> int: + def default_value_string(self) -> str: """Python representation of the default value for Enums. As per the spec, this is the first value of the Enum. @@ -572,11 +560,11 @@ def __post_init__(self) -> None: super().__post_init__() # check for unset fields @property - def proto_name(self): + def proto_name(self) -> str: return self.proto_obj.name @property - def py_name(self): + def py_name(self) -> str: return pythonize_class_name(self.proto_name) @@ -628,7 +616,7 @@ def mutable_default_args(self) -> Dict[str, str]: Name and actual default value (as a string) for each argument with mutable default values. """ - mutable_default_args = dict() + mutable_default_args = {} if self.py_input_message: for f in self.py_input_message.fields: @@ -654,18 +642,15 @@ def proto_name(self) -> str: @property def route(self) -> str: - return ( - f"/{self.output_file.package}." - f"{self.parent.proto_name}/{self.proto_name}" - ) + return f"/{self.output_file.package}.{self.parent.proto_name}/{self.proto_name}" @property - def py_input_message(self) -> Union[None, MessageCompiler]: + def py_input_message(self) -> Optional[MessageCompiler]: """Find the input message object. Returns ------- - Union[None, MessageCompiler] + Optional[MessageCompiler] Method instance representing the input message. If not input message could be found or there are no input messages, None is returned. @@ -685,14 +670,13 @@ def py_input_message(self) -> Union[None, MessageCompiler]: @property def py_input_message_type(self) -> str: - """String representation of the Python type correspoding to the + """String representation of the Python type corresponding to the input message. Returns ------- str - String representation of the Python type correspoding to the - input message. + String representation of the Python type corresponding to the input message. """ return get_type_reference( package=self.output_file.package, @@ -702,14 +686,13 @@ def py_input_message_type(self) -> str: @property def py_output_message_type(self) -> str: - """String representation of the Python type correspoding to the + """String representation of the Python type corresponding to the output message. Returns ------- str - String representation of the Python type correspoding to the - output message. + String representation of the Python type corresponding to the output message. """ return get_type_reference( package=self.output_file.package, diff --git a/src/betterproto/plugin/parser.py b/src/betterproto/plugin/parser.py index cb5b65449..a1be2685e 100644 --- a/src/betterproto/plugin/parser.py +++ b/src/betterproto/plugin/parser.py @@ -1,7 +1,7 @@ import itertools import pathlib import sys -from typing import List, Iterator +from typing import TYPE_CHECKING, Iterator, List, Tuple, Union, Set try: # betterproto[compiler] specific dependencies @@ -13,10 +13,9 @@ ServiceDescriptorProto, ) except ImportError as err: - missing_import = err.args[0][17:-1] print( "\033[31m" - f"Unable to import `{missing_import}` from betterproto plugin! " + f"Unable to import `{err.name}` from betterproto plugin! " "Please ensure that you've installed betterproto as " '`pip install "betterproto[compiler]"` so that compiler dependencies ' "are included." @@ -24,26 +23,32 @@ ) raise SystemExit(1) -from betterproto.plugin.models import ( - PluginRequestCompiler, - OutputTemplate, - MessageCompiler, +from .compiler import outputfile_compiler +from .models import ( + EnumDefinitionCompiler, FieldCompiler, - OneOfFieldCompiler, MapEntryCompiler, - EnumDefinitionCompiler, + MessageCompiler, + OneOfFieldCompiler, + OutputTemplate, + PluginRequestCompiler, ServiceCompiler, ServiceMethodCompiler, is_map, is_oneof, ) -from betterproto.plugin.compiler import outputfile_compiler +if TYPE_CHECKING: + from google.protobuf.descriptor import Descriptor -def traverse(proto_file: FieldDescriptorProto) -> Iterator: +def traverse( + proto_file: FieldDescriptorProto, +) -> "itertools.chain[Tuple[Union[str, EnumDescriptorProto], List[int]]]": # Todo: Keep information about nested hierarchy - def _traverse(path, items, prefix=""): + def _traverse( + path: List[int], items: List["Descriptor"], prefix="" + ) -> Iterator[Tuple[Union[str, EnumDescriptorProto], List[int]]]: for i, item in enumerate(items): # Adjust the name since we flatten the hierarchy. # Todo: don't change the name, but include full name in returned tuple @@ -104,7 +109,7 @@ def generate_code( read_protobuf_service(service, index, output_package) # Generate output files - output_paths: pathlib.Path = set() + output_paths: Set[pathlib.Path] = set() for output_package_name, output_package in request_data.output_packages.items(): # Add files to the response object @@ -112,20 +117,17 @@ def generate_code( output_paths.add(output_path) f: response.File = response.file.add() - f.name: str = str(output_path) + f.name = str(output_path) # Render and then format the output file - f.content: str = outputfile_compiler(output_file=output_package) + f.content = outputfile_compiler(output_file=output_package) # Make each output directory a package with __init__ file - init_files = ( - set( - directory.joinpath("__init__.py") - for path in output_paths - for directory in path.parents - ) - - output_paths - ) + init_files = { + directory.joinpath("__init__.py") + for path in output_paths + for directory in path.parents + } - output_paths for init_file in init_files: init = response.file.add() diff --git a/src/betterproto/templates/template.py.j2 b/src/betterproto/templates/template.py.j2 index 753d340c7..07262fae2 100644 --- a/src/betterproto/templates/template.py.j2 +++ b/src/betterproto/templates/template.py.j2 @@ -4,7 +4,6 @@ {% for i in output_file.python_module_imports|sort %} import {{ i }} {% endfor %} -from dataclasses import dataclass {% if output_file.datetime_imports %} from datetime import {% for i in output_file.datetime_imports|sort %}{{ i }}{% if not loop.last %}, {% endif %}{% endfor %} @@ -37,7 +36,6 @@ class {{ enum.py_name }}(betterproto.Enum): {% endfor %} {% endif %} {% for message in output_file.messages %} -@dataclass(eq=False, repr=False) class {{ message.py_name }}(betterproto.Message): {% if message.comment %} {{ message.comment }} @@ -53,12 +51,11 @@ class {{ message.py_name }}(betterproto.Message): pass {% endif %} - {% if message.deprecated or message.deprecated_fields %} + {% if message.deprecated or message.has_deprecated_fields %} def __post_init__(self) -> None: {% if message.deprecated %} warnings.warn("{{ message.py_name }} is deprecated", DeprecationWarning) {% endif %} - super().__post_init__() {% for field in message.deprecated_fields %} if self.{{ field }}: warnings.warn("{{ message.py_name }}.{{ field }} is deprecated", DeprecationWarning) @@ -82,7 +79,7 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): Optional[{{ field.annotation }}] {%- else -%} {{ field.annotation }} - {%- endif -%} = + {%- endif -%} = {%- if field.py_name not in method.mutable_default_args -%} {{ field.default_value_string }} {%- else -%} diff --git a/tests/grpc/test_stream_stream.py b/tests/grpc/test_stream_stream.py index 2fc923781..01ca2b849 100644 --- a/tests/grpc/test_stream_stream.py +++ b/tests/grpc/test_stream_stream.py @@ -1,12 +1,10 @@ import asyncio import betterproto from betterproto.grpc.util.async_channel import AsyncChannel -from dataclasses import dataclass import pytest from typing import AsyncIterator -@dataclass class Message(betterproto.Message): body: str = betterproto.string_field(1) @@ -27,10 +25,7 @@ async def connect(self, requests: AsyncIterator): async def to_list(generator: AsyncIterator): - result = [] - async for value in generator: - result.append(value) - return result + return [value async for value in generator] @pytest.fixture diff --git a/tests/mocks.py b/tests/mocks.py index 9042f78f1..dc6e11720 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -6,7 +6,7 @@ class MockChannel(Channel): # noinspection PyMissingConstructor def __init__(self, responses=None) -> None: - self.responses = responses if responses else [] + self.responses = responses or [] self.requests = [] self._loop = None diff --git a/tests/test_features.py b/tests/test_features.py index f5482643c..d9e08236f 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -1,14 +1,11 @@ import betterproto -from dataclasses import dataclass from typing import Optional, List, Dict def test_has_field(): - @dataclass class Bar(betterproto.Message): baz: int = betterproto.int32_field(1) - @dataclass class Foo(betterproto.Message): bar: Bar = betterproto.message_field(1) @@ -32,7 +29,6 @@ class Foo(betterproto.Message): foo.bar = Bar() assert betterproto.serialized_on_wire(foo.bar) is False - @dataclass class WithCollections(betterproto.Message): test_list: List[str] = betterproto.string_field(1) test_map: Dict[str, str] = betterproto.map_field( @@ -53,11 +49,9 @@ class WithCollections(betterproto.Message): def test_class_init(): - @dataclass class Bar(betterproto.Message): name: str = betterproto.string_field(1) - @dataclass class Foo(betterproto.Message): name: str = betterproto.string_field(1) child: Bar = betterproto.message_field(2) @@ -72,7 +66,6 @@ class TestEnum(betterproto.Enum): ZERO = 0 ONE = 1 - @dataclass class Foo(betterproto.Message): bar: TestEnum = betterproto.enum_field(1) @@ -86,13 +79,11 @@ class Foo(betterproto.Message): def test_unknown_fields(): - @dataclass class Newer(betterproto.Message): foo: bool = betterproto.bool_field(1) bar: int = betterproto.int32_field(2) baz: str = betterproto.string_field(3) - @dataclass class Older(betterproto.Message): foo: bool = betterproto.bool_field(1) @@ -108,11 +99,9 @@ class Older(betterproto.Message): def test_oneof_support(): - @dataclass class Sub(betterproto.Message): val: int = betterproto.int32_field(1) - @dataclass class Foo(betterproto.Message): bar: int = betterproto.int32_field(1, group="group1") baz: str = betterproto.string_field(2, group="group1") @@ -153,7 +142,6 @@ class Foo(betterproto.Message): def test_json_casing(): - @dataclass class CasingTest(betterproto.Message): pascal_case: int = betterproto.int32_field(1) camel_case: int = betterproto.int32_field(2) @@ -184,7 +172,6 @@ class CasingTest(betterproto.Message): def test_optional_flag(): - @dataclass class Request(betterproto.Message): flag: Optional[bool] = betterproto.message_field(1, wraps=betterproto.TYPE_BOOL) @@ -199,7 +186,6 @@ class Request(betterproto.Message): def test_to_dict_default_values(): - @dataclass class TestMessage(betterproto.Message): some_int: int = betterproto.int32_field(1) some_double: float = betterproto.double_field(2) @@ -229,7 +215,6 @@ class TestMessage(betterproto.Message): } # Some default and some other values - @dataclass class TestMessage2(betterproto.Message): some_int: int = betterproto.int32_field(1) some_double: float = betterproto.double_field(2) @@ -265,11 +250,9 @@ class TestMessage2(betterproto.Message): } # Nested messages - @dataclass class TestChildMessage(betterproto.Message): some_other_int: int = betterproto.int32_field(1) - @dataclass class TestParentMessage(betterproto.Message): some_int: int = betterproto.int32_field(1) some_double: float = betterproto.double_field(2) @@ -285,7 +268,6 @@ class TestParentMessage(betterproto.Message): def test_oneof_default_value_set_causes_writes_wire(): - @dataclass class Foo(betterproto.Message): bar: int = betterproto.int32_field(1, group="group1") baz: str = betterproto.string_field(2, group="group1") diff --git a/tests/util.py b/tests/util.py index d085eb636..6c631417c 100644 --- a/tests/util.py +++ b/tests/util.py @@ -23,8 +23,7 @@ def get_files(path, suffix: str) -> Generator[str, None, None]: def get_directories(path): for root, directories, files in os.walk(path): - for directory in directories: - yield directory + yield from directories async def protoc( @@ -49,7 +48,7 @@ async def protoc( def get_test_case_json_data(test_case_name: str, json_file_name: Optional[str] = None): - test_data_file_name = json_file_name if json_file_name else f"{test_case_name}.json" + test_data_file_name = json_file_name or f"{test_case_name}.json" test_data_file_path = inputs_path.joinpath(test_case_name, test_data_file_name) if not test_data_file_path.exists(): @@ -77,7 +76,7 @@ def find_module( module_path = pathlib.Path(*module.__path__) - for sub in list(sub.parent for sub in module_path.glob("**/__init__.py")): + for sub in [sub.parent for sub in module_path.glob("**/__init__.py")]: if sub == module_path: continue sub_module_path = sub.relative_to(module_path)