Ausgabe der neuen DB Einträge
This commit is contained in:
parent
bad48e1627
commit
cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions
560
venv/lib/python3.9/site-packages/stone/backends/python_client.py
Normal file
560
venv/lib/python3.9/site-packages/stone/backends/python_client.py
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from stone.backend import CodeBackend
|
||||
from stone.backends.helpers import fmt_underscores
|
||||
from stone.backends.python_helpers import (
|
||||
check_route_name_conflict,
|
||||
fmt_class,
|
||||
fmt_func,
|
||||
fmt_namespace,
|
||||
fmt_obj,
|
||||
fmt_type,
|
||||
fmt_var,
|
||||
)
|
||||
from stone.backends.python_types import (
|
||||
class_name_for_data_type,
|
||||
)
|
||||
from stone.ir import (
|
||||
is_nullable_type,
|
||||
is_list_type,
|
||||
is_map_type,
|
||||
is_struct_type,
|
||||
is_tag_ref,
|
||||
is_union_type,
|
||||
is_user_defined_type,
|
||||
is_void_type,
|
||||
)
|
||||
|
||||
_MYPY = False
|
||||
if _MYPY:
|
||||
import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression
|
||||
|
||||
# Hack to get around some of Python 2's standard library modules that
|
||||
# accept ascii-encodable unicode literals in lieu of strs, but where
|
||||
# actually passing such literals results in errors with mypy --py2. See
|
||||
# <https://github.com/python/typeshed/issues/756> and
|
||||
# <https://github.com/python/mypy/issues/2536>.
|
||||
import importlib
|
||||
argparse = importlib.import_module(str('argparse')) # type: typing.Any
|
||||
|
||||
|
||||
# This will be at the top of the generated file.
|
||||
base = """\
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-generated by Stone, do not modify.
|
||||
# flake8: noqa
|
||||
# pylint: skip-file
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
"""
|
||||
|
||||
# Matches format of Babel doc tags
|
||||
doc_sub_tag_re = re.compile(':(?P<tag>[A-z]*):`(?P<val>.*?)`')
|
||||
|
||||
DOCSTRING_CLOSE_RESPONSE = """\
|
||||
If you do not consume the entire response body, then you must call close on the
|
||||
response object, otherwise you will max out your available connections. We
|
||||
recommend using the `contextlib.closing
|
||||
<https://docs.python.org/2/library/contextlib.html#contextlib.closing>`_
|
||||
context manager to ensure this."""
|
||||
|
||||
_cmdline_parser = argparse.ArgumentParser(
|
||||
prog='python-client-backend',
|
||||
description=(
|
||||
'Generates a Python class with a method for each route. Extend the '
|
||||
'generated class and implement the abstract request() method. This '
|
||||
'class assumes that the python_types backend was used with the same '
|
||||
'output directory.'),
|
||||
)
|
||||
_cmdline_parser.add_argument(
|
||||
'-m',
|
||||
'--module-name',
|
||||
required=True,
|
||||
type=str,
|
||||
help=('The name of the Python module to generate. Please exclude the .py '
|
||||
'file extension.'),
|
||||
)
|
||||
_cmdline_parser.add_argument(
|
||||
'-c',
|
||||
'--class-name',
|
||||
required=True,
|
||||
type=str,
|
||||
help='The name of the Python class that contains each route as a method.',
|
||||
)
|
||||
_cmdline_parser.add_argument(
|
||||
'-t',
|
||||
'--types-package',
|
||||
required=True,
|
||||
type=str,
|
||||
help='The output Python package of the python_types backend.',
|
||||
)
|
||||
_cmdline_parser.add_argument(
|
||||
'-e',
|
||||
'--error-class-path',
|
||||
default='.exceptions.ApiError',
|
||||
type=str,
|
||||
help=(
|
||||
"The path to the class that's raised when a route returns an error. "
|
||||
"The class name is inserted into the doc for route methods."),
|
||||
)
|
||||
_cmdline_parser.add_argument(
|
||||
'-w',
|
||||
'--auth-type',
|
||||
type=str,
|
||||
help='The auth type of the client to generate.',
|
||||
)
|
||||
|
||||
|
||||
class PythonClientBackend(CodeBackend):
|
||||
|
||||
cmdline_parser = _cmdline_parser
|
||||
supported_auth_types = None
|
||||
|
||||
def generate(self, api):
|
||||
"""Generates a module called "base".
|
||||
|
||||
The module will contain a base class that will have a method for
|
||||
each route across all namespaces.
|
||||
"""
|
||||
|
||||
with self.output_to_relative_path('%s.py' % self.args.module_name):
|
||||
self.emit_raw(base)
|
||||
# Import "warnings" if any of the routes are deprecated.
|
||||
found_deprecated = False
|
||||
for namespace in api.namespaces.values():
|
||||
for route in namespace.routes:
|
||||
if route.deprecated:
|
||||
self.emit('import warnings')
|
||||
found_deprecated = True
|
||||
break
|
||||
if found_deprecated:
|
||||
break
|
||||
self.emit()
|
||||
self._generate_imports(api.namespaces.values())
|
||||
self.emit()
|
||||
self.emit() # PEP-8 expects two-blank lines before class def
|
||||
self.emit('class %s(object):' % self.args.class_name)
|
||||
with self.indent():
|
||||
self.emit('__metaclass__ = ABCMeta')
|
||||
self.emit()
|
||||
self.emit('@abstractmethod')
|
||||
self.emit(
|
||||
'def request(self, route, namespace, arg, arg_binary=None):')
|
||||
with self.indent():
|
||||
self.emit('pass')
|
||||
self.emit()
|
||||
self._generate_route_methods(api.namespaces.values())
|
||||
|
||||
def _generate_imports(self, namespaces):
|
||||
# Only import namespaces that have user-defined types defined.
|
||||
for namespace in namespaces:
|
||||
if namespace.data_types:
|
||||
self.emit('from {} import {}'.format(self.args.types_package, fmt_namespace(namespace.name)))
|
||||
|
||||
def _generate_route_methods(self, namespaces):
|
||||
"""Creates methods for the routes in each namespace. All data types
|
||||
and routes are represented as Python classes."""
|
||||
self.cur_namespace = None
|
||||
for namespace in namespaces:
|
||||
if namespace.routes:
|
||||
self.emit('# ------------------------------------------')
|
||||
self.emit('# Routes in {} namespace'.format(namespace.name))
|
||||
self.emit()
|
||||
self._generate_routes(namespace)
|
||||
|
||||
def _generate_routes(self, namespace):
|
||||
"""
|
||||
Generates Python methods that correspond to routes in the namespace.
|
||||
"""
|
||||
|
||||
# Hack: needed for _docf()
|
||||
self.cur_namespace = namespace
|
||||
# list of auth_types supported in this base class.
|
||||
# this is passed with the new -w flag
|
||||
if self.args.auth_type is not None:
|
||||
self.supported_auth_types = [auth_type.strip().lower() for auth_type in self.args.auth_type.split(',')]
|
||||
|
||||
check_route_name_conflict(namespace)
|
||||
|
||||
for route in namespace.routes:
|
||||
# compatibility mode : included routes are passed by whitelist
|
||||
# actual auth attr inluded in the route is ignored in this mode.
|
||||
if self.supported_auth_types is None:
|
||||
self._generate_route_helper(namespace, route)
|
||||
if route.attrs.get('style') == 'download':
|
||||
self._generate_route_helper(namespace, route, True)
|
||||
else:
|
||||
route_auth_attr = None
|
||||
if route.attrs is not None:
|
||||
route_auth_attr = route.attrs.get('auth')
|
||||
if route_auth_attr is None:
|
||||
continue
|
||||
route_auth_modes = [mode.strip().lower() for mode in route_auth_attr.split(',')]
|
||||
for base_auth_type in self.supported_auth_types:
|
||||
if base_auth_type in route_auth_modes:
|
||||
self._generate_route_helper(namespace, route)
|
||||
if route.attrs.get('style') == 'download':
|
||||
self._generate_route_helper(namespace, route, True)
|
||||
break # to avoid duplicate method declaration in the same base class
|
||||
|
||||
def _generate_route_helper(self, namespace, route, download_to_file=False):
|
||||
"""Generate a Python method that corresponds to a route.
|
||||
|
||||
:param namespace: Namespace that the route belongs to.
|
||||
:param stone.ir.ApiRoute route: IR node for the route.
|
||||
:param bool download_to_file: Whether a special version of the route
|
||||
that downloads the response body to a file should be generated.
|
||||
This can only be used for download-style routes.
|
||||
"""
|
||||
arg_data_type = route.arg_data_type
|
||||
result_data_type = route.result_data_type
|
||||
|
||||
request_binary_body = route.attrs.get('style') == 'upload'
|
||||
response_binary_body = route.attrs.get('style') == 'download'
|
||||
|
||||
if download_to_file:
|
||||
assert response_binary_body, 'download_to_file can only be set ' \
|
||||
'for download-style routes.'
|
||||
self._generate_route_method_decl(namespace,
|
||||
route,
|
||||
arg_data_type,
|
||||
request_binary_body,
|
||||
method_name_suffix='_to_file',
|
||||
extra_args=['download_path'])
|
||||
else:
|
||||
self._generate_route_method_decl(namespace,
|
||||
route,
|
||||
arg_data_type,
|
||||
request_binary_body)
|
||||
|
||||
with self.indent():
|
||||
extra_request_args = None
|
||||
extra_return_arg = None
|
||||
footer = None
|
||||
if request_binary_body:
|
||||
extra_request_args = [('f',
|
||||
'bytes',
|
||||
'Contents to upload.')]
|
||||
elif download_to_file:
|
||||
extra_request_args = [('download_path',
|
||||
'str',
|
||||
'Path on local machine to save file.')]
|
||||
if response_binary_body and not download_to_file:
|
||||
extra_return_arg = ':class:`requests.models.Response`'
|
||||
footer = DOCSTRING_CLOSE_RESPONSE
|
||||
|
||||
if route.doc:
|
||||
func_docstring = self.process_doc(route.doc, self._docf)
|
||||
else:
|
||||
func_docstring = None
|
||||
|
||||
self._generate_docstring_for_func(
|
||||
namespace,
|
||||
arg_data_type,
|
||||
result_data_type,
|
||||
route.error_data_type,
|
||||
overview=func_docstring,
|
||||
extra_request_args=extra_request_args,
|
||||
extra_return_arg=extra_return_arg,
|
||||
footer=footer,
|
||||
)
|
||||
|
||||
self._maybe_generate_deprecation_warning(route)
|
||||
|
||||
# Code to instantiate a class for the request data type
|
||||
if is_void_type(arg_data_type):
|
||||
self.emit('arg = None')
|
||||
elif is_struct_type(arg_data_type):
|
||||
self.generate_multiline_list(
|
||||
[f.name for f in arg_data_type.all_fields],
|
||||
before='arg = {}.{}'.format(
|
||||
fmt_namespace(arg_data_type.namespace.name),
|
||||
fmt_class(arg_data_type.name)),
|
||||
)
|
||||
elif not is_union_type(arg_data_type):
|
||||
raise AssertionError('Unhandled request type %r' %
|
||||
arg_data_type)
|
||||
|
||||
# Code to make the request
|
||||
args = [
|
||||
'{}.{}'.format(fmt_namespace(namespace.name),
|
||||
fmt_func(route.name, version=route.version)),
|
||||
"'{}'".format(namespace.name),
|
||||
'arg']
|
||||
if request_binary_body:
|
||||
args.append('f')
|
||||
else:
|
||||
args.append('None')
|
||||
self.generate_multiline_list(args, 'r = self.request', compact=False)
|
||||
|
||||
if download_to_file:
|
||||
self.emit('self._save_body_to_file(download_path, r[1])')
|
||||
if is_void_type(result_data_type):
|
||||
self.emit('return None')
|
||||
else:
|
||||
self.emit('return r[0]')
|
||||
else:
|
||||
if is_void_type(result_data_type):
|
||||
self.emit('return None')
|
||||
else:
|
||||
self.emit('return r')
|
||||
self.emit()
|
||||
|
||||
def _generate_route_method_decl(
|
||||
self, namespace, route, arg_data_type, request_binary_body,
|
||||
method_name_suffix='', extra_args=None):
|
||||
"""Generates the method prototype for a route."""
|
||||
args = ['self']
|
||||
if extra_args:
|
||||
args += extra_args
|
||||
if request_binary_body:
|
||||
args.append('f')
|
||||
if is_struct_type(arg_data_type):
|
||||
for field in arg_data_type.all_fields:
|
||||
if is_nullable_type(field.data_type):
|
||||
args.append('{}=None'.format(field.name))
|
||||
elif field.has_default:
|
||||
# TODO(kelkabany): Decide whether we really want to set the
|
||||
# default in the argument list. This will send the default
|
||||
# over the wire even if it isn't overridden. The benefit is
|
||||
# it locks in a default even if it is changed server-side.
|
||||
if is_user_defined_type(field.data_type):
|
||||
ns = field.data_type.namespace
|
||||
else:
|
||||
ns = None
|
||||
arg = '{}={}'.format(
|
||||
field.name,
|
||||
self._generate_python_value(ns, field.default))
|
||||
args.append(arg)
|
||||
else:
|
||||
args.append(field.name)
|
||||
elif is_union_type(arg_data_type):
|
||||
args.append('arg')
|
||||
elif not is_void_type(arg_data_type):
|
||||
raise AssertionError('Unhandled request type: %r' %
|
||||
arg_data_type)
|
||||
|
||||
method_name = fmt_func(route.name + method_name_suffix, version=route.version)
|
||||
namespace_name = fmt_underscores(namespace.name)
|
||||
self.generate_multiline_list(args, 'def {}_{}'.format(namespace_name, method_name), ':')
|
||||
|
||||
def _maybe_generate_deprecation_warning(self, route):
|
||||
if route.deprecated:
|
||||
msg = '{} is deprecated.'.format(route.name)
|
||||
if route.deprecated.by:
|
||||
msg += ' Use {}.'.format(route.deprecated.by.name)
|
||||
args = ["'{}'".format(msg), 'DeprecationWarning']
|
||||
self.generate_multiline_list(
|
||||
args,
|
||||
before='warnings.warn',
|
||||
delim=('(', ')'),
|
||||
compact=False,
|
||||
)
|
||||
|
||||
def _generate_docstring_for_func(self, namespace, arg_data_type,
|
||||
result_data_type=None, error_data_type=None,
|
||||
overview=None, extra_request_args=None,
|
||||
extra_return_arg=None, footer=None):
|
||||
"""
|
||||
Generates a docstring for a function or method.
|
||||
|
||||
This function is versatile. It will create a docstring using all the
|
||||
data that is provided.
|
||||
|
||||
:param arg_data_type: The data type describing the argument to the
|
||||
route. The data type should be a struct, and each field will be
|
||||
treated as an input parameter of the method.
|
||||
:param result_data_type: The data type of the route result.
|
||||
:param error_data_type: The data type of the route result in the case
|
||||
of an error.
|
||||
:param str overview: A description of the route that will be located
|
||||
at the top of the docstring.
|
||||
:param extra_request_args: [(field name, field type, field doc), ...]
|
||||
Describes any additional parameters for the method that aren't a
|
||||
field in arg_data_type.
|
||||
:param str extra_return_arg: Name of an additional return type that. If
|
||||
this is specified, it is assumed that the return of the function
|
||||
will be a tuple of return_data_type and extra_return-arg.
|
||||
:param str footer: Additional notes at the end of the docstring.
|
||||
"""
|
||||
fields = [] if is_void_type(arg_data_type) else arg_data_type.fields
|
||||
if not fields and not overview:
|
||||
# If we don't have an overview or any input parameters, we skip the
|
||||
# docstring altogether.
|
||||
return
|
||||
|
||||
self.emit('"""')
|
||||
if overview:
|
||||
self.emit_wrapped_text(overview)
|
||||
|
||||
# Description of all input parameters
|
||||
if extra_request_args or fields:
|
||||
if overview:
|
||||
# Add a blank line if we had an overview
|
||||
self.emit()
|
||||
|
||||
if extra_request_args:
|
||||
for name, data_type_name, doc in extra_request_args:
|
||||
if data_type_name:
|
||||
field_doc = ':param {} {}: {}'.format(data_type_name,
|
||||
name, doc)
|
||||
self.emit_wrapped_text(field_doc,
|
||||
subsequent_prefix=' ')
|
||||
else:
|
||||
self.emit_wrapped_text(
|
||||
':param {}: {}'.format(name, doc),
|
||||
subsequent_prefix=' ')
|
||||
|
||||
if is_struct_type(arg_data_type):
|
||||
for field in fields:
|
||||
if field.doc:
|
||||
if is_user_defined_type(field.data_type):
|
||||
field_doc = ':param {}: {}'.format(
|
||||
field.name, self.process_doc(field.doc, self._docf))
|
||||
else:
|
||||
field_doc = ':param {} {}: {}'.format(
|
||||
self._format_type_in_doc(namespace, field.data_type),
|
||||
field.name,
|
||||
self.process_doc(field.doc, self._docf),
|
||||
)
|
||||
self.emit_wrapped_text(
|
||||
field_doc, subsequent_prefix=' ')
|
||||
if is_user_defined_type(field.data_type):
|
||||
# It's clearer to declare the type of a composite on
|
||||
# a separate line since it references a class in
|
||||
# another module
|
||||
self.emit(':type {}: {}'.format(
|
||||
field.name,
|
||||
self._format_type_in_doc(namespace, field.data_type),
|
||||
))
|
||||
else:
|
||||
# If the field has no docstring, then just document its
|
||||
# type.
|
||||
field_doc = ':type {}: {}'.format(
|
||||
field.name,
|
||||
self._format_type_in_doc(namespace, field.data_type),
|
||||
)
|
||||
self.emit_wrapped_text(field_doc)
|
||||
|
||||
elif is_union_type(arg_data_type):
|
||||
if arg_data_type.doc:
|
||||
self.emit_wrapped_text(':param arg: {}'.format(
|
||||
self.process_doc(arg_data_type.doc, self._docf)),
|
||||
subsequent_prefix=' ')
|
||||
self.emit(':type arg: {}'.format(
|
||||
self._format_type_in_doc(namespace, arg_data_type)))
|
||||
|
||||
if overview and not (extra_request_args or fields):
|
||||
# Only output an empty line if we had an overview and haven't
|
||||
# started a section on declaring types.
|
||||
self.emit()
|
||||
|
||||
if extra_return_arg:
|
||||
# Special case where the function returns a tuple. The first
|
||||
# element is the JSON response. The second element is the
|
||||
# the extra_return_arg param.
|
||||
args = []
|
||||
if is_void_type(result_data_type):
|
||||
args.append('None')
|
||||
else:
|
||||
rtype = self._format_type_in_doc(namespace,
|
||||
result_data_type)
|
||||
args.append(rtype)
|
||||
args.append(extra_return_arg)
|
||||
self.generate_multiline_list(args, ':rtype: ')
|
||||
else:
|
||||
if is_void_type(result_data_type):
|
||||
self.emit(':rtype: None')
|
||||
else:
|
||||
rtype = self._format_type_in_doc(namespace, result_data_type)
|
||||
self.emit(':rtype: {}'.format(rtype))
|
||||
|
||||
if not is_void_type(error_data_type) and error_data_type.fields:
|
||||
self.emit(':raises: :class:`{}`'.format(self.args.error_class_path))
|
||||
self.emit()
|
||||
# To provide more clarity to a dev who reads the docstring, suggest
|
||||
# the route's error class. This is confusing, however, because we
|
||||
# don't know where the error object that's raised will store
|
||||
# the more detailed route error defined in stone.
|
||||
error_class_name = self.args.error_class_path.rsplit('.', 1)[-1]
|
||||
self.emit('If this raises, {} will contain:'.format(error_class_name))
|
||||
with self.indent():
|
||||
self.emit(self._format_type_in_doc(namespace, error_data_type))
|
||||
|
||||
if footer:
|
||||
self.emit()
|
||||
self.emit_wrapped_text(footer)
|
||||
self.emit('"""')
|
||||
|
||||
def _docf(self, tag, val):
|
||||
"""
|
||||
Callback used as the handler argument to process_docs(). This converts
|
||||
Babel doc references to Sphinx-friendly annotations.
|
||||
"""
|
||||
if tag == 'type':
|
||||
fq_val = val
|
||||
if '.' not in val:
|
||||
fq_val = self.cur_namespace.name + '.' + fq_val
|
||||
return ':class:`{}.{}`'.format(self.args.types_package, fq_val)
|
||||
elif tag == 'route':
|
||||
if ':' in val:
|
||||
val, version = val.split(':', 1)
|
||||
version = int(version)
|
||||
else:
|
||||
version = 1
|
||||
if '.' in val:
|
||||
return ':meth:`{}`'.format(fmt_func(val, version=version))
|
||||
else:
|
||||
return ':meth:`{}_{}`'.format(
|
||||
self.cur_namespace.name, fmt_func(val, version=version))
|
||||
elif tag == 'link':
|
||||
anchor, link = val.rsplit(' ', 1)
|
||||
return '`{} <{}>`_'.format(anchor, link)
|
||||
elif tag == 'val':
|
||||
if val == 'null':
|
||||
return 'None'
|
||||
elif val == 'true' or val == 'false':
|
||||
return '``{}``'.format(val.capitalize())
|
||||
else:
|
||||
return val
|
||||
elif tag == 'field':
|
||||
return '``{}``'.format(val)
|
||||
else:
|
||||
raise RuntimeError('Unknown doc ref tag %r' % tag)
|
||||
|
||||
def _format_type_in_doc(self, namespace, data_type):
|
||||
"""
|
||||
Returns a string that can be recognized by Sphinx as a type reference
|
||||
in a docstring.
|
||||
"""
|
||||
if is_void_type(data_type):
|
||||
return 'None'
|
||||
elif is_user_defined_type(data_type):
|
||||
return ':class:`{}.{}.{}`'.format(
|
||||
self.args.types_package, namespace.name, fmt_type(data_type))
|
||||
elif is_nullable_type(data_type):
|
||||
return 'Nullable[{}]'.format(
|
||||
self._format_type_in_doc(namespace, data_type.data_type),
|
||||
)
|
||||
elif is_list_type(data_type):
|
||||
return 'List[{}]'.format(
|
||||
self._format_type_in_doc(namespace, data_type.data_type),
|
||||
)
|
||||
elif is_map_type(data_type):
|
||||
return 'Map[{}, {}]'.format(
|
||||
self._format_type_in_doc(namespace, data_type.key_data_type),
|
||||
self._format_type_in_doc(namespace, data_type.value_data_type),
|
||||
)
|
||||
else:
|
||||
return fmt_type(data_type)
|
||||
|
||||
def _generate_python_value(self, namespace, value):
|
||||
if is_tag_ref(value):
|
||||
return '{}.{}.{}'.format(
|
||||
fmt_namespace(namespace.name),
|
||||
class_name_for_data_type(value.union_data_type),
|
||||
fmt_var(value.tag_name))
|
||||
else:
|
||||
return fmt_obj(value)
|
||||
Loading…
Add table
Add a link
Reference in a new issue