Add Documentation (#125)
Add sphinx docs with readthedocs integration. Docs can be built locally with `poe docs`.
This commit is contained in:
parent
58556e0eb6
commit
d3e4fbb311
10
.github/workflows/code-quality.yml
vendored
10
.github/workflows/code-quality.yml
vendored
@ -9,8 +9,8 @@ on:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
black:
|
||||
name: Black
|
||||
check-formatting:
|
||||
name: Check code/doc formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -18,3 +18,9 @@ jobs:
|
||||
uses: lgeiger/black-action@master
|
||||
with:
|
||||
args: --check src/ tests/
|
||||
|
||||
- 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
|
||||
|
17
.readthedocs.yml
Normal file
17
.readthedocs.yml
Normal file
@ -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
|
@ -326,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
|
||||
|
||||
|
31
docs/api.rst
Normal file
31
docs/api.rst
Normal file
@ -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__
|
||||
|
||||
|
||||
.. autofunction:: betterproto.serialized_on_wire
|
||||
|
||||
.. autofunction:: betterproto.which_one_of
|
||||
|
||||
|
||||
Enumerations
|
||||
-------------
|
||||
|
||||
.. autoclass:: betterproto.Enum()
|
||||
:members:
|
||||
|
||||
|
||||
.. autoclass:: betterproto.Casing()
|
||||
:members:
|
60
docs/conf.py
Normal file
60
docs/conf.py
Normal file
@ -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"
|
33
docs/index.rst
Normal file
33
docs/index.rst
Normal file
@ -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`
|
157
docs/migrating.rst
Normal file
157
docs/migrating.rst
Normal file
@ -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``
|
192
docs/quick-start.rst
Normal file
192
docs/quick-start.rst
Normal file
@ -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 <https://grpclib.readthedocs.io/en/latest>`_ 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!"
|
@ -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`
|
308
poetry.lock
generated
308
poetry.lock
generated
@ -1,3 +1,11 @@
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A configurable sidebar-enabled Sphinx theme"
|
||||
name = "alabaster"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.7.12"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
@ -43,6 +51,17 @@ 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]]
|
||||
category = "dev"
|
||||
description = "Internationalization utilities"
|
||||
name = "babel"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.8.0"
|
||||
|
||||
[package.dependencies]
|
||||
pytz = ">=2015.7"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Backport of Python 3.7's datetime.fromisoformat"
|
||||
@ -148,7 +167,7 @@ description = "Code coverage measurement for Python"
|
||||
name = "coverage"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
version = "5.2.1"
|
||||
version = "5.3"
|
||||
|
||||
[package.extras]
|
||||
toml = ["toml"]
|
||||
@ -182,6 +201,14 @@ optional = false
|
||||
python-versions = "*"
|
||||
version = "0.3.1"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Docutils -- Python Documentation Utilities"
|
||||
name = "docutils"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "0.16"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A platform independent file lock."
|
||||
@ -276,6 +303,14 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.10"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Getting image size from png/jpeg/jpeg2000/gif file"
|
||||
name = "imagesize"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "1.2.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Read metadata from Python packages"
|
||||
@ -389,7 +424,7 @@ description = "Bring colors to your terminal."
|
||||
name = "pastel"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
@ -453,7 +488,7 @@ description = "Pygments is a syntax highlighting package written in Python."
|
||||
name = "pygments"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
@ -532,6 +567,14 @@ pytest = ">=5.0"
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox", "pytest-asyncio"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
name = "pytz"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2020.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
@ -566,6 +609,131 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "1.15.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms."
|
||||
name = "snowballstemmer"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2.0.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python documentation generator"
|
||||
name = "sphinx"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "3.1.2"
|
||||
|
||||
[package.dependencies]
|
||||
Jinja2 = ">=2.3"
|
||||
Pygments = ">=2.0"
|
||||
alabaster = ">=0.7,<0.8"
|
||||
babel = ">=1.3"
|
||||
colorama = ">=0.3.5"
|
||||
docutils = ">=0.12"
|
||||
imagesize = "*"
|
||||
packaging = "*"
|
||||
requests = ">=2.5.0"
|
||||
setuptools = "*"
|
||||
snowballstemmer = ">=1.1"
|
||||
sphinxcontrib-applehelp = "*"
|
||||
sphinxcontrib-devhelp = "*"
|
||||
sphinxcontrib-htmlhelp = "*"
|
||||
sphinxcontrib-jsmath = "*"
|
||||
sphinxcontrib-qthelp = "*"
|
||||
sphinxcontrib-serializinghtml = "*"
|
||||
|
||||
[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]]
|
||||
category = "dev"
|
||||
description = "Read the Docs theme for Sphinx"
|
||||
name = "sphinx-rtd-theme"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.5.0"
|
||||
|
||||
[package.dependencies]
|
||||
sphinx = "*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
|
||||
name = "sphinxcontrib-applehelp"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "1.0.2"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
|
||||
name = "sphinxcontrib-devhelp"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "1.0.2"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
|
||||
name = "sphinxcontrib-htmlhelp"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "1.0.3"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest", "html5lib"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A sphinx extension which renders display math in HTML via JavaScript"
|
||||
name = "sphinxcontrib-jsmath"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "1.0.1"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest", "flake8", "mypy"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
|
||||
name = "sphinxcontrib-qthelp"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "1.0.3"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
|
||||
name = "sphinxcontrib-serializinghtml"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "1.1.4"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
@ -680,11 +848,15 @@ testing = ["jaraco.itertools", "func-timeout"]
|
||||
compiler = ["black", "jinja2", "protobuf"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "2a56c1e83222f20c06385fd175a00e158419e11780c14ca5153b23e1dfa3d651"
|
||||
content-hash = "aa1cdf753b393b5b61a5794989b6883709d82dd13e924c1b1d92d93c21dd256c"
|
||||
lock-version = "1.0"
|
||||
python-versions = "^3.6"
|
||||
|
||||
[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"},
|
||||
@ -700,6 +872,10 @@ attrs = [
|
||||
{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"},
|
||||
]
|
||||
@ -733,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"},
|
||||
@ -780,6 +956,10 @@ 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"},
|
||||
@ -904,6 +1084,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"},
|
||||
@ -999,8 +1183,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"},
|
||||
@ -1039,8 +1223,8 @@ py = [
|
||||
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
|
||||
]
|
||||
pygments = [
|
||||
{file = "Pygments-2.7.0-py3-none-any.whl", hash = "sha256:2df50d16b45b977217e02cba6c8422aaddb859f3d0570a88e09b00eafae89c6e"},
|
||||
{file = "Pygments-2.7.0.tar.gz", hash = "sha256:2594e8fdb06fef91552f86f4fd3a244d148ab24b66042036e64f29a291515048"},
|
||||
{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"},
|
||||
@ -1061,6 +1245,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"},
|
||||
@ -1092,6 +1280,42 @@ 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"},
|
||||
|
@ -33,6 +33,8 @@ 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]
|
||||
@ -48,6 +50,7 @@ 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
|
||||
|
@ -120,8 +120,8 @@ DATETIME_ZERO = 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,26 @@ 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:
|
||||
@ -497,9 +512,15 @@ class ProtoClassMetadata:
|
||||
|
||||
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.
|
||||
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__`.
|
||||
"""
|
||||
|
||||
_serialized_on_wire: bool
|
||||
@ -605,7 +626,7 @@ class Message(ABC):
|
||||
|
||||
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 +705,20 @@ class Message(ABC):
|
||||
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:
|
||||
@ -788,6 +822,16 @@ class Message(ABC):
|
||||
"""
|
||||
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 +882,47 @@ class Message(ABC):
|
||||
# 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.
|
||||
Returns a JSON serializable dict representation of this object.
|
||||
|
||||
`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`.
|
||||
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()
|
||||
@ -938,10 +1009,20 @@ class Message(ABC):
|
||||
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:
|
||||
@ -998,28 +1079,70 @@ class Message(ABC):
|
||||
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."""
|
||||
"""
|
||||
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)
|
||||
|
@ -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 snake_case(value: str, strict: bool = True):
|
||||
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 pascal_case(value: str, strict: bool = True):
|
||||
)
|
||||
|
||||
|
||||
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:]
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user