mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-28 09:51:36 +00:00
[no ci] Delete src/gam/googleapiclient.shouldnotbehere directory
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
import logging
|
||||
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
@@ -1,167 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Helpers for authentication using oauth2client or google-auth."""
|
||||
|
||||
import httplib2
|
||||
|
||||
try:
|
||||
import google.auth
|
||||
import google.auth.credentials
|
||||
|
||||
HAS_GOOGLE_AUTH = True
|
||||
except ImportError: # pragma: NO COVER
|
||||
HAS_GOOGLE_AUTH = False
|
||||
|
||||
try:
|
||||
import google_auth_httplib2
|
||||
except ImportError: # pragma: NO COVER
|
||||
google_auth_httplib2 = None
|
||||
|
||||
try:
|
||||
import oauth2client
|
||||
import oauth2client.client
|
||||
|
||||
HAS_OAUTH2CLIENT = True
|
||||
except ImportError: # pragma: NO COVER
|
||||
HAS_OAUTH2CLIENT = False
|
||||
|
||||
|
||||
def credentials_from_file(filename, scopes=None, quota_project_id=None):
|
||||
"""Returns credentials loaded from a file."""
|
||||
if HAS_GOOGLE_AUTH:
|
||||
credentials, _ = google.auth.load_credentials_from_file(
|
||||
filename, scopes=scopes, quota_project_id=quota_project_id
|
||||
)
|
||||
return credentials
|
||||
else:
|
||||
raise EnvironmentError(
|
||||
"client_options.credentials_file is only supported in google-auth."
|
||||
)
|
||||
|
||||
|
||||
def default_credentials(scopes=None, quota_project_id=None):
|
||||
"""Returns Application Default Credentials."""
|
||||
if HAS_GOOGLE_AUTH:
|
||||
credentials, _ = google.auth.default(
|
||||
scopes=scopes, quota_project_id=quota_project_id
|
||||
)
|
||||
return credentials
|
||||
elif HAS_OAUTH2CLIENT:
|
||||
if scopes is not None or quota_project_id is not None:
|
||||
raise EnvironmentError(
|
||||
"client_options.scopes and client_options.quota_project_id are not supported in oauth2client."
|
||||
"Please install google-auth."
|
||||
)
|
||||
return oauth2client.client.GoogleCredentials.get_application_default()
|
||||
else:
|
||||
raise EnvironmentError(
|
||||
"No authentication library is available. Please install either "
|
||||
"google-auth or oauth2client."
|
||||
)
|
||||
|
||||
|
||||
def with_scopes(credentials, scopes):
|
||||
"""Scopes the credentials if necessary.
|
||||
|
||||
Args:
|
||||
credentials (Union[
|
||||
google.auth.credentials.Credentials,
|
||||
oauth2client.client.Credentials]): The credentials to scope.
|
||||
scopes (Sequence[str]): The list of scopes.
|
||||
|
||||
Returns:
|
||||
Union[google.auth.credentials.Credentials,
|
||||
oauth2client.client.Credentials]: The scoped credentials.
|
||||
"""
|
||||
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
||||
return google.auth.credentials.with_scopes_if_required(credentials, scopes)
|
||||
else:
|
||||
try:
|
||||
if credentials.create_scoped_required():
|
||||
return credentials.create_scoped(scopes)
|
||||
else:
|
||||
return credentials
|
||||
except AttributeError:
|
||||
return credentials
|
||||
|
||||
|
||||
def authorized_http(credentials):
|
||||
"""Returns an http client that is authorized with the given credentials.
|
||||
|
||||
Args:
|
||||
credentials (Union[
|
||||
google.auth.credentials.Credentials,
|
||||
oauth2client.client.Credentials]): The credentials to use.
|
||||
|
||||
Returns:
|
||||
Union[httplib2.Http, google_auth_httplib2.AuthorizedHttp]: An
|
||||
authorized http client.
|
||||
"""
|
||||
from googleapiclient.http import build_http
|
||||
|
||||
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
||||
if google_auth_httplib2 is None:
|
||||
raise ValueError(
|
||||
"Credentials from google.auth specified, but "
|
||||
"google-api-python-client is unable to use these credentials "
|
||||
"unless google-auth-httplib2 is installed. Please install "
|
||||
"google-auth-httplib2."
|
||||
)
|
||||
return google_auth_httplib2.AuthorizedHttp(credentials, http=build_http())
|
||||
else:
|
||||
return credentials.authorize(build_http())
|
||||
|
||||
|
||||
def refresh_credentials(credentials):
|
||||
# Refresh must use a new http instance, as the one associated with the
|
||||
# credentials could be a AuthorizedHttp or an oauth2client-decorated
|
||||
# Http instance which would cause a weird recursive loop of refreshing
|
||||
# and likely tear a hole in spacetime.
|
||||
refresh_http = httplib2.Http()
|
||||
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
||||
request = google_auth_httplib2.Request(refresh_http)
|
||||
return credentials.refresh(request)
|
||||
else:
|
||||
return credentials.refresh(refresh_http)
|
||||
|
||||
|
||||
def apply_credentials(credentials, headers):
|
||||
# oauth2client and google-auth have the same interface for this.
|
||||
if not is_valid(credentials):
|
||||
refresh_credentials(credentials)
|
||||
return credentials.apply(headers)
|
||||
|
||||
|
||||
def is_valid(credentials):
|
||||
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
||||
return credentials.valid
|
||||
else:
|
||||
return (
|
||||
credentials.access_token is not None
|
||||
and not credentials.access_token_expired
|
||||
)
|
||||
|
||||
|
||||
def get_credentials_from_http(http):
|
||||
if http is None:
|
||||
return None
|
||||
elif hasattr(http.request, "credentials"):
|
||||
return http.request.credentials
|
||||
elif hasattr(http, "credentials") and not isinstance(
|
||||
http.credentials, httplib2.Credentials
|
||||
):
|
||||
return http.credentials
|
||||
else:
|
||||
return None
|
||||
@@ -1,207 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Helper functions for commonly used utilities."""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
POSITIONAL_WARNING = "WARNING"
|
||||
POSITIONAL_EXCEPTION = "EXCEPTION"
|
||||
POSITIONAL_IGNORE = "IGNORE"
|
||||
POSITIONAL_SET = frozenset(
|
||||
[POSITIONAL_WARNING, POSITIONAL_EXCEPTION, POSITIONAL_IGNORE]
|
||||
)
|
||||
|
||||
positional_parameters_enforcement = POSITIONAL_WARNING
|
||||
|
||||
_SYM_LINK_MESSAGE = "File: {0}: Is a symbolic link."
|
||||
_IS_DIR_MESSAGE = "{0}: Is a directory"
|
||||
_MISSING_FILE_MESSAGE = "Cannot access {0}: No such file or directory"
|
||||
|
||||
|
||||
def positional(max_positional_args):
|
||||
"""A decorator to declare that only the first N arguments may be positional.
|
||||
|
||||
This decorator makes it easy to support Python 3 style keyword-only
|
||||
parameters. For example, in Python 3 it is possible to write::
|
||||
|
||||
def fn(pos1, *, kwonly1=None, kwonly2=None):
|
||||
...
|
||||
|
||||
All named parameters after ``*`` must be a keyword::
|
||||
|
||||
fn(10, 'kw1', 'kw2') # Raises exception.
|
||||
fn(10, kwonly1='kw1') # Ok.
|
||||
|
||||
Example
|
||||
^^^^^^^
|
||||
|
||||
To define a function like above, do::
|
||||
|
||||
@positional(1)
|
||||
def fn(pos1, kwonly1=None, kwonly2=None):
|
||||
...
|
||||
|
||||
If no default value is provided to a keyword argument, it becomes a
|
||||
required keyword argument::
|
||||
|
||||
@positional(0)
|
||||
def fn(required_kw):
|
||||
...
|
||||
|
||||
This must be called with the keyword parameter::
|
||||
|
||||
fn() # Raises exception.
|
||||
fn(10) # Raises exception.
|
||||
fn(required_kw=10) # Ok.
|
||||
|
||||
When defining instance or class methods always remember to account for
|
||||
``self`` and ``cls``::
|
||||
|
||||
class MyClass(object):
|
||||
|
||||
@positional(2)
|
||||
def my_method(self, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@positional(2)
|
||||
def my_method(cls, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
The positional decorator behavior is controlled by
|
||||
``_helpers.positional_parameters_enforcement``, which may be set to
|
||||
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
|
||||
``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
|
||||
nothing, respectively, if a declaration is violated.
|
||||
|
||||
Args:
|
||||
max_positional_arguments: Maximum number of positional arguments. All
|
||||
parameters after this index must be
|
||||
keyword only.
|
||||
|
||||
Returns:
|
||||
A decorator that prevents using arguments after max_positional_args
|
||||
from being used as positional parameters.
|
||||
|
||||
Raises:
|
||||
TypeError: if a keyword-only argument is provided as a positional
|
||||
parameter, but only if
|
||||
_helpers.positional_parameters_enforcement is set to
|
||||
POSITIONAL_EXCEPTION.
|
||||
"""
|
||||
|
||||
def positional_decorator(wrapped):
|
||||
@functools.wraps(wrapped)
|
||||
def positional_wrapper(*args, **kwargs):
|
||||
if len(args) > max_positional_args:
|
||||
plural_s = ""
|
||||
if max_positional_args != 1:
|
||||
plural_s = "s"
|
||||
message = (
|
||||
"{function}() takes at most {args_max} positional "
|
||||
"argument{plural} ({args_given} given)".format(
|
||||
function=wrapped.__name__,
|
||||
args_max=max_positional_args,
|
||||
args_given=len(args),
|
||||
plural=plural_s,
|
||||
)
|
||||
)
|
||||
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
|
||||
raise TypeError(message)
|
||||
elif positional_parameters_enforcement == POSITIONAL_WARNING:
|
||||
logger.warning(message)
|
||||
return wrapped(*args, **kwargs)
|
||||
|
||||
return positional_wrapper
|
||||
|
||||
if isinstance(max_positional_args, int):
|
||||
return positional_decorator
|
||||
else:
|
||||
args, _, _, defaults, _, _, _ = inspect.getfullargspec(max_positional_args)
|
||||
return positional(len(args) - len(defaults))(max_positional_args)
|
||||
|
||||
|
||||
def parse_unique_urlencoded(content):
|
||||
"""Parses unique key-value parameters from urlencoded content.
|
||||
|
||||
Args:
|
||||
content: string, URL-encoded key-value pairs.
|
||||
|
||||
Returns:
|
||||
dict, The key-value pairs from ``content``.
|
||||
|
||||
Raises:
|
||||
ValueError: if one of the keys is repeated.
|
||||
"""
|
||||
urlencoded_params = urllib.parse.parse_qs(content)
|
||||
params = {}
|
||||
for key, value in urlencoded_params.items():
|
||||
if len(value) != 1:
|
||||
msg = "URL-encoded content contains a repeated value:" "%s -> %s" % (
|
||||
key,
|
||||
", ".join(value),
|
||||
)
|
||||
raise ValueError(msg)
|
||||
params[key] = value[0]
|
||||
return params
|
||||
|
||||
|
||||
def update_query_params(uri, params):
|
||||
"""Updates a URI with new query parameters.
|
||||
|
||||
If a given key from ``params`` is repeated in the ``uri``, then
|
||||
the URI will be considered invalid and an error will occur.
|
||||
|
||||
If the URI is valid, then each value from ``params`` will
|
||||
replace the corresponding value in the query parameters (if
|
||||
it exists).
|
||||
|
||||
Args:
|
||||
uri: string, A valid URI, with potential existing query parameters.
|
||||
params: dict, A dictionary of query parameters.
|
||||
|
||||
Returns:
|
||||
The same URI but with the new query parameters added.
|
||||
"""
|
||||
parts = urllib.parse.urlparse(uri)
|
||||
query_params = parse_unique_urlencoded(parts.query)
|
||||
query_params.update(params)
|
||||
new_query = urllib.parse.urlencode(query_params)
|
||||
new_parts = parts._replace(query=new_query)
|
||||
return urllib.parse.urlunparse(new_parts)
|
||||
|
||||
|
||||
def _add_query_parameter(url, name, value):
|
||||
"""Adds a query parameter to a url.
|
||||
|
||||
Replaces the current value if it already exists in the URL.
|
||||
|
||||
Args:
|
||||
url: string, url to add the query parameter to.
|
||||
name: string, query parameter name.
|
||||
value: string, query parameter value.
|
||||
|
||||
Returns:
|
||||
Updated query parameter. Does not update the url if value is None.
|
||||
"""
|
||||
if value is None:
|
||||
return url
|
||||
else:
|
||||
return update_query_params(url, {name: value})
|
||||
@@ -1,315 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Channel notifications support.
|
||||
|
||||
Classes and functions to support channel subscriptions and notifications
|
||||
on those channels.
|
||||
|
||||
Notes:
|
||||
- This code is based on experimental APIs and is subject to change.
|
||||
- Notification does not do deduplication of notification ids, that's up to
|
||||
the receiver.
|
||||
- Storing the Channel between calls is up to the caller.
|
||||
|
||||
|
||||
Example setting up a channel:
|
||||
|
||||
# Create a new channel that gets notifications via webhook.
|
||||
channel = new_webhook_channel("https://example.com/my_web_hook")
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it before calling the
|
||||
# watch method because notifications may start arriving before the watch
|
||||
# method returns.
|
||||
...
|
||||
|
||||
resp = service.objects().watchAll(
|
||||
bucket="some_bucket_id", body=channel.body()).execute()
|
||||
channel.update(resp)
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it after being updated
|
||||
# since the resource_id value will now be correct, and that's needed to
|
||||
# stop a subscription.
|
||||
...
|
||||
|
||||
|
||||
An example Webhook implementation using webapp2. Note that webapp2 puts
|
||||
headers in a case insensitive dictionary, as headers aren't guaranteed to
|
||||
always be upper case.
|
||||
|
||||
id = self.request.headers[X_GOOG_CHANNEL_ID]
|
||||
|
||||
# Retrieve the channel by id.
|
||||
channel = ...
|
||||
|
||||
# Parse notification from the headers, including validating the id.
|
||||
n = notification_from_headers(channel, self.request.headers)
|
||||
|
||||
# Do app specific stuff with the notification here.
|
||||
if n.resource_state == 'sync':
|
||||
# Code to handle sync state.
|
||||
elif n.resource_state == 'exists':
|
||||
# Code to handle the exists state.
|
||||
elif n.resource_state == 'not_exists':
|
||||
# Code to handle the not exists state.
|
||||
|
||||
|
||||
Example of unsubscribing.
|
||||
|
||||
service.channels().stop(channel.body()).execute()
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from googleapiclient import _helpers as util
|
||||
from googleapiclient import errors
|
||||
|
||||
# The unix time epoch starts at midnight 1970.
|
||||
EPOCH = datetime.datetime(1970, 1, 1)
|
||||
|
||||
# Map the names of the parameters in the JSON channel description to
|
||||
# the parameter names we use in the Channel class.
|
||||
CHANNEL_PARAMS = {
|
||||
"address": "address",
|
||||
"id": "id",
|
||||
"expiration": "expiration",
|
||||
"params": "params",
|
||||
"resourceId": "resource_id",
|
||||
"resourceUri": "resource_uri",
|
||||
"type": "type",
|
||||
"token": "token",
|
||||
}
|
||||
|
||||
X_GOOG_CHANNEL_ID = "X-GOOG-CHANNEL-ID"
|
||||
X_GOOG_MESSAGE_NUMBER = "X-GOOG-MESSAGE-NUMBER"
|
||||
X_GOOG_RESOURCE_STATE = "X-GOOG-RESOURCE-STATE"
|
||||
X_GOOG_RESOURCE_URI = "X-GOOG-RESOURCE-URI"
|
||||
X_GOOG_RESOURCE_ID = "X-GOOG-RESOURCE-ID"
|
||||
|
||||
|
||||
def _upper_header_keys(headers):
|
||||
new_headers = {}
|
||||
for k, v in headers.items():
|
||||
new_headers[k.upper()] = v
|
||||
return new_headers
|
||||
|
||||
|
||||
class Notification(object):
|
||||
"""A Notification from a Channel.
|
||||
|
||||
Notifications are not usually constructed directly, but are returned
|
||||
from functions like notification_from_headers().
|
||||
|
||||
Attributes:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored.
|
||||
uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The unique identifier of the version of the resource at
|
||||
this event.
|
||||
"""
|
||||
|
||||
@util.positional(5)
|
||||
def __init__(self, message_number, state, resource_uri, resource_id):
|
||||
"""Notification constructor.
|
||||
|
||||
Args:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored. Can be one
|
||||
of "exists", "not_exists", or "sync".
|
||||
resource_uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The identifier of the watched resource.
|
||||
"""
|
||||
self.message_number = message_number
|
||||
self.state = state
|
||||
self.resource_uri = resource_uri
|
||||
self.resource_id = resource_id
|
||||
|
||||
|
||||
class Channel(object):
|
||||
"""A Channel for notifications.
|
||||
|
||||
Usually not constructed directly, instead it is returned from helper
|
||||
functions like new_webhook_channel().
|
||||
|
||||
Attributes:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
|
||||
@util.positional(5)
|
||||
def __init__(
|
||||
self,
|
||||
type,
|
||||
id,
|
||||
token,
|
||||
address,
|
||||
expiration=None,
|
||||
params=None,
|
||||
resource_id="",
|
||||
resource_uri="",
|
||||
):
|
||||
"""Create a new Channel.
|
||||
|
||||
In user code, this Channel constructor will not typically be called
|
||||
manually since there are functions for creating channels for each specific
|
||||
type with a more customized set of arguments to pass.
|
||||
|
||||
Args:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.token = token
|
||||
self.address = address
|
||||
self.expiration = expiration
|
||||
self.params = params
|
||||
self.resource_id = resource_id
|
||||
self.resource_uri = resource_uri
|
||||
|
||||
def body(self):
|
||||
"""Build a body from the Channel.
|
||||
|
||||
Constructs a dictionary that's appropriate for passing into watch()
|
||||
methods as the value of body argument.
|
||||
|
||||
Returns:
|
||||
A dictionary representation of the channel.
|
||||
"""
|
||||
result = {
|
||||
"id": self.id,
|
||||
"token": self.token,
|
||||
"type": self.type,
|
||||
"address": self.address,
|
||||
}
|
||||
if self.params:
|
||||
result["params"] = self.params
|
||||
if self.resource_id:
|
||||
result["resourceId"] = self.resource_id
|
||||
if self.resource_uri:
|
||||
result["resourceUri"] = self.resource_uri
|
||||
if self.expiration:
|
||||
result["expiration"] = self.expiration
|
||||
|
||||
return result
|
||||
|
||||
def update(self, resp):
|
||||
"""Update a channel with information from the response of watch().
|
||||
|
||||
When a request is sent to watch() a resource, the response returned
|
||||
from the watch() request is a dictionary with updated channel information,
|
||||
such as the resource_id, which is needed when stopping a subscription.
|
||||
|
||||
Args:
|
||||
resp: dict, The response from a watch() method.
|
||||
"""
|
||||
for json_name, param_name in CHANNEL_PARAMS.items():
|
||||
value = resp.get(json_name)
|
||||
if value is not None:
|
||||
setattr(self, param_name, value)
|
||||
|
||||
|
||||
def notification_from_headers(channel, headers):
|
||||
"""Parse a notification from the webhook request headers, validate
|
||||
the notification, and return a Notification object.
|
||||
|
||||
Args:
|
||||
channel: Channel, The channel that the notification is associated with.
|
||||
headers: dict, A dictionary like object that contains the request headers
|
||||
from the webhook HTTP request.
|
||||
|
||||
Returns:
|
||||
A Notification object.
|
||||
|
||||
Raises:
|
||||
errors.InvalidNotificationError if the notification is invalid.
|
||||
ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int.
|
||||
"""
|
||||
headers = _upper_header_keys(headers)
|
||||
channel_id = headers[X_GOOG_CHANNEL_ID]
|
||||
if channel.id != channel_id:
|
||||
raise errors.InvalidNotificationError(
|
||||
"Channel id mismatch: %s != %s" % (channel.id, channel_id)
|
||||
)
|
||||
else:
|
||||
message_number = int(headers[X_GOOG_MESSAGE_NUMBER])
|
||||
state = headers[X_GOOG_RESOURCE_STATE]
|
||||
resource_uri = headers[X_GOOG_RESOURCE_URI]
|
||||
resource_id = headers[X_GOOG_RESOURCE_ID]
|
||||
return Notification(message_number, state, resource_uri, resource_id)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def new_webhook_channel(url, token=None, expiration=None, params=None):
|
||||
"""Create a new webhook Channel.
|
||||
|
||||
Args:
|
||||
url: str, URL to post notifications to.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each notification delivered
|
||||
over this channel.
|
||||
expiration: datetime.datetime, A time in the future when the channel
|
||||
should expire. Can also be None if the subscription should use the
|
||||
default expiration. Note that different services may have different
|
||||
limits on how long a subscription lasts. Check the response from the
|
||||
watch() method to see the value the service has set for an expiration
|
||||
time.
|
||||
params: dict, Extra parameters to pass on channel creation. Currently
|
||||
not used for webhook channels.
|
||||
"""
|
||||
expiration_ms = 0
|
||||
if expiration:
|
||||
delta = expiration - EPOCH
|
||||
expiration_ms = (
|
||||
delta.microseconds / 1000 + (delta.seconds + delta.days * 24 * 3600) * 1000
|
||||
)
|
||||
if expiration_ms < 0:
|
||||
expiration_ms = 0
|
||||
|
||||
return Channel(
|
||||
"web_hook",
|
||||
str(uuid.uuid4()),
|
||||
token,
|
||||
url,
|
||||
expiration=expiration_ms,
|
||||
params=params,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,78 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Caching utility for the discovery document."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_DOC_MAX_AGE = 60 * 60 * 24 # 1 day
|
||||
DISCOVERY_DOC_DIR = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), "documents"
|
||||
)
|
||||
|
||||
|
||||
def autodetect():
|
||||
"""Detects an appropriate cache module and returns it.
|
||||
|
||||
Returns:
|
||||
googleapiclient.discovery_cache.base.Cache, a cache object which
|
||||
is auto detected, or None if no cache object is available.
|
||||
"""
|
||||
if "GAE_ENV" in os.environ:
|
||||
try:
|
||||
from . import appengine_memcache
|
||||
|
||||
return appengine_memcache.cache
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from . import file_cache
|
||||
|
||||
return file_cache.cache
|
||||
except Exception:
|
||||
LOGGER.info(
|
||||
"file_cache is only supported with oauth2client<4.0.0", exc_info=False
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_static_doc(serviceName, version):
|
||||
"""Retrieves the discovery document from the directory defined in
|
||||
DISCOVERY_DOC_DIR corresponding to the serviceName and version provided.
|
||||
|
||||
Args:
|
||||
serviceName: string, name of the service.
|
||||
version: string, the version of the service.
|
||||
|
||||
Returns:
|
||||
A string containing the contents of the JSON discovery document,
|
||||
otherwise None if the JSON discovery document was not found.
|
||||
"""
|
||||
|
||||
content = None
|
||||
doc_name = "{}.{}.json".format(serviceName, version)
|
||||
|
||||
try:
|
||||
with open(os.path.join(DISCOVERY_DOC_DIR, doc_name), "r") as f:
|
||||
content = f.read()
|
||||
except FileNotFoundError:
|
||||
# File does not exist. Nothing to do here.
|
||||
pass
|
||||
|
||||
return content
|
||||
@@ -1,55 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""App Engine memcache based cache for the discovery document."""
|
||||
|
||||
import logging
|
||||
|
||||
# This is only an optional dependency because we only import this
|
||||
# module when google.appengine.api.memcache is available.
|
||||
from google.appengine.api import memcache
|
||||
|
||||
from . import base
|
||||
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NAMESPACE = "google-api-client"
|
||||
|
||||
|
||||
class Cache(base.Cache):
|
||||
"""A cache with app engine memcache API."""
|
||||
|
||||
def __init__(self, max_age):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
max_age: Cache expiration in seconds.
|
||||
"""
|
||||
self._max_age = max_age
|
||||
|
||||
def get(self, url):
|
||||
try:
|
||||
return memcache.get(url, namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
|
||||
def set(self, url, content):
|
||||
try:
|
||||
memcache.set(url, content, time=int(self._max_age), namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
|
||||
|
||||
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
|
||||
@@ -1,46 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""An abstract class for caching the discovery document."""
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class Cache(object):
|
||||
"""A base abstract cache class."""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, url):
|
||||
"""Gets the content from the memcache with a given key.
|
||||
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
|
||||
Returns:
|
||||
object, the value in the cache for the given key, or None if the key is
|
||||
not in the cache.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def set(self, url, content):
|
||||
"""Sets the given key and content in the cache.
|
||||
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
content: string, the discovery document.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -1,145 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""File based cache for the discovery document.
|
||||
|
||||
The cache is stored in a single file so that multiple processes can
|
||||
share the same cache. It locks the file whenever accessing to the
|
||||
file. When the cache content is corrupted, it will be initialized with
|
||||
an empty cache.
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
from oauth2client.contrib.locked_file import LockedFile
|
||||
except ImportError:
|
||||
# oauth2client < 2.0.0
|
||||
try:
|
||||
from oauth2client.locked_file import LockedFile
|
||||
except ImportError:
|
||||
# oauth2client > 4.0.0 or google-auth
|
||||
raise ImportError(
|
||||
"file_cache is unavailable when using oauth2client >= 4.0.0 or google-auth"
|
||||
)
|
||||
|
||||
from . import base
|
||||
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FILENAME = "google-api-python-client-discovery-doc.cache"
|
||||
EPOCH = datetime.datetime(1970, 1, 1)
|
||||
|
||||
|
||||
def _to_timestamp(date):
|
||||
try:
|
||||
return (date - EPOCH).total_seconds()
|
||||
except AttributeError:
|
||||
# The following is the equivalent of total_seconds() in Python2.6.
|
||||
# See also: https://docs.python.org/2/library/datetime.html
|
||||
delta = date - EPOCH
|
||||
return (
|
||||
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6
|
||||
) / 10**6
|
||||
|
||||
|
||||
def _read_or_initialize_cache(f):
|
||||
f.file_handle().seek(0)
|
||||
try:
|
||||
cache = json.load(f.file_handle())
|
||||
except Exception:
|
||||
# This means it opens the file for the first time, or the cache is
|
||||
# corrupted, so initializing the file with an empty dict.
|
||||
cache = {}
|
||||
f.file_handle().truncate(0)
|
||||
f.file_handle().seek(0)
|
||||
json.dump(cache, f.file_handle())
|
||||
return cache
|
||||
|
||||
|
||||
class Cache(base.Cache):
|
||||
"""A file based cache for the discovery documents."""
|
||||
|
||||
def __init__(self, max_age):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
max_age: Cache expiration in seconds.
|
||||
"""
|
||||
self._max_age = max_age
|
||||
self._file = os.path.join(tempfile.gettempdir(), FILENAME)
|
||||
f = LockedFile(self._file, "a+", "r")
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
_read_or_initialize_cache(f)
|
||||
# If we can not obtain the lock, other process or thread must
|
||||
# have initialized the file.
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
def get(self, url):
|
||||
f = LockedFile(self._file, "r+", "r")
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
cache = _read_or_initialize_cache(f)
|
||||
if url in cache:
|
||||
content, t = cache.get(url, (None, 0))
|
||||
if _to_timestamp(datetime.datetime.now()) < t + self._max_age:
|
||||
return content
|
||||
return None
|
||||
else:
|
||||
LOGGER.debug("Could not obtain a lock for the cache file.")
|
||||
return None
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
def set(self, url, content):
|
||||
f = LockedFile(self._file, "r+", "r")
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
cache = _read_or_initialize_cache(f)
|
||||
cache[url] = (content, _to_timestamp(datetime.datetime.now()))
|
||||
# Remove stale cache.
|
||||
for k, (_, timestamp) in list(cache.items()):
|
||||
if (
|
||||
_to_timestamp(datetime.datetime.now())
|
||||
>= timestamp + self._max_age
|
||||
):
|
||||
del cache[k]
|
||||
f.file_handle().truncate(0)
|
||||
f.file_handle().seek(0)
|
||||
json.dump(cache, f.file_handle())
|
||||
else:
|
||||
LOGGER.debug("Could not obtain a lock for the cache file.")
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
|
||||
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
|
||||
@@ -1,197 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Errors for the library.
|
||||
|
||||
All exceptions defined by the library
|
||||
should be defined in this file.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "jcgregorio@google.com (Joe Gregorio)"
|
||||
|
||||
import json
|
||||
|
||||
from googleapiclient import _helpers as util
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HttpError(Error):
|
||||
"""HTTP data was invalid or unexpected."""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, resp, content, uri=None):
|
||||
self.resp = resp
|
||||
if not isinstance(content, bytes):
|
||||
raise TypeError("HTTP content should be bytes")
|
||||
self.content = content
|
||||
self.uri = uri
|
||||
self.error_details = ""
|
||||
self.reason = self._get_reason()
|
||||
|
||||
@property
|
||||
def status_code(self):
|
||||
"""Return the HTTP status code from the response content."""
|
||||
return self.resp.status
|
||||
|
||||
def _get_reason(self):
|
||||
"""Calculate the reason for the error from the response content."""
|
||||
reason = self.resp.reason
|
||||
try:
|
||||
try:
|
||||
data = json.loads(self.content.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
# In case it is not json
|
||||
data = self.content.decode("utf-8")
|
||||
if isinstance(data, dict):
|
||||
reason = data["error"]["message"]
|
||||
error_detail_keyword = next(
|
||||
(
|
||||
kw
|
||||
for kw in ["detail", "details", "errors", "message"]
|
||||
if kw in data["error"]
|
||||
),
|
||||
"",
|
||||
)
|
||||
if error_detail_keyword:
|
||||
self.error_details = data["error"][error_detail_keyword]
|
||||
elif isinstance(data, list) and len(data) > 0:
|
||||
first_error = data[0]
|
||||
reason = first_error["error"]["message"]
|
||||
if "details" in first_error["error"]:
|
||||
self.error_details = first_error["error"]["details"]
|
||||
else:
|
||||
self.error_details = data
|
||||
except (ValueError, KeyError, TypeError):
|
||||
pass
|
||||
if reason is None:
|
||||
reason = ""
|
||||
return reason.strip()
|
||||
|
||||
def __repr__(self):
|
||||
if self.error_details:
|
||||
return '<HttpError %s when requesting %s returned "%s". Details: "%s">' % (
|
||||
self.resp.status,
|
||||
self.uri,
|
||||
self.reason,
|
||||
self.error_details,
|
||||
)
|
||||
elif self.uri:
|
||||
return '<HttpError %s when requesting %s returned "%s">' % (
|
||||
self.resp.status,
|
||||
self.uri,
|
||||
self.reason,
|
||||
)
|
||||
else:
|
||||
return '<HttpError %s "%s">' % (self.resp.status, self.reason)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
class InvalidJsonError(Error):
|
||||
"""The JSON returned could not be parsed."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownFileType(Error):
|
||||
"""File type unknown or unexpected."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownLinkType(Error):
|
||||
"""Link type unknown or unexpected."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownApiNameOrVersion(Error):
|
||||
"""No API with that name and version exists."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnacceptableMimeTypeError(Error):
|
||||
"""That is an unacceptable mimetype for this operation."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MediaUploadSizeError(Error):
|
||||
"""Media is larger than the method can accept."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ResumableUploadError(HttpError):
|
||||
"""Error occurred during resumable upload."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidChunkSizeError(Error):
|
||||
"""The given chunksize is not valid."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidNotificationError(Error):
|
||||
"""The channel Notification is invalid."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BatchError(HttpError):
|
||||
"""Error occurred during batch operations."""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, reason, resp=None, content=None):
|
||||
self.resp = resp
|
||||
self.content = content
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self):
|
||||
if getattr(self.resp, "status", None) is None:
|
||||
return '<BatchError "%s">' % (self.reason)
|
||||
else:
|
||||
return '<BatchError %s "%s">' % (self.resp.status, self.reason)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
class UnexpectedMethodError(Error):
|
||||
"""Exception raised by RequestMockBuilder on unexpected calls."""
|
||||
|
||||
@util.positional(1)
|
||||
def __init__(self, methodId=None):
|
||||
"""Constructor for an UnexpectedMethodError."""
|
||||
super(UnexpectedMethodError, self).__init__(
|
||||
"Received unexpected call %s" % methodId
|
||||
)
|
||||
|
||||
|
||||
class UnexpectedBodyError(Error):
|
||||
"""Exception raised by RequestMockBuilder on unexpected bodies."""
|
||||
|
||||
def __init__(self, expected, provided):
|
||||
"""Constructor for an UnexpectedMethodError."""
|
||||
super(UnexpectedBodyError, self).__init__(
|
||||
"Expected: [%s] - Provided: [%s]" % (expected, provided)
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,183 +0,0 @@
|
||||
# Copyright 2014 Joe Gregorio
|
||||
#
|
||||
# Licensed under the MIT License
|
||||
|
||||
"""MIME-Type Parser
|
||||
|
||||
This module provides basic functions for handling mime-types. It can handle
|
||||
matching mime-types against a list of media-ranges. See section 14.1 of the
|
||||
HTTP specification [RFC 2616] for a complete explanation.
|
||||
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
||||
|
||||
Contents:
|
||||
- parse_mime_type(): Parses a mime-type into its component parts.
|
||||
- parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q'
|
||||
quality parameter.
|
||||
- quality(): Determines the quality ('q') of a mime-type when
|
||||
compared against a list of media-ranges.
|
||||
- quality_parsed(): Just like quality() except the second parameter must be
|
||||
pre-parsed.
|
||||
- best_match(): Choose the mime-type with the highest quality ('q')
|
||||
from a list of candidates.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from functools import reduce
|
||||
|
||||
__version__ = "0.1.3"
|
||||
__author__ = "Joe Gregorio"
|
||||
__email__ = "joe@bitworking.org"
|
||||
__license__ = "MIT License"
|
||||
__credits__ = ""
|
||||
|
||||
|
||||
def parse_mime_type(mime_type):
|
||||
"""Parses a mime-type into its component parts.
|
||||
|
||||
Carves up a mime-type and returns a tuple of the (type, subtype, params)
|
||||
where 'params' is a dictionary of all the parameters for the media range.
|
||||
For example, the media range 'application/xhtml;q=0.5' would get parsed
|
||||
into:
|
||||
|
||||
('application', 'xhtml', {'q', '0.5'})
|
||||
"""
|
||||
parts = mime_type.split(";")
|
||||
params = dict(
|
||||
[tuple([s.strip() for s in param.split("=", 1)]) for param in parts[1:]]
|
||||
)
|
||||
full_type = parts[0].strip()
|
||||
# Java URLConnection class sends an Accept header that includes a
|
||||
# single '*'. Turn it into a legal wildcard.
|
||||
if full_type == "*":
|
||||
full_type = "*/*"
|
||||
(type, subtype) = full_type.split("/")
|
||||
|
||||
return (type.strip(), subtype.strip(), params)
|
||||
|
||||
|
||||
def parse_media_range(range):
|
||||
"""Parse a media-range into its component parts.
|
||||
|
||||
Carves up a media range and returns a tuple of the (type, subtype,
|
||||
params) where 'params' is a dictionary of all the parameters for the media
|
||||
range. For example, the media range 'application/*;q=0.5' would get parsed
|
||||
into:
|
||||
|
||||
('application', '*', {'q', '0.5'})
|
||||
|
||||
In addition this function also guarantees that there is a value for 'q'
|
||||
in the params dictionary, filling it in with a proper default if
|
||||
necessary.
|
||||
"""
|
||||
(type, subtype, params) = parse_mime_type(range)
|
||||
if (
|
||||
"q" not in params
|
||||
or not params["q"]
|
||||
or not float(params["q"])
|
||||
or float(params["q"]) > 1
|
||||
or float(params["q"]) < 0
|
||||
):
|
||||
params["q"] = "1"
|
||||
|
||||
return (type, subtype, params)
|
||||
|
||||
|
||||
def fitness_and_quality_parsed(mime_type, parsed_ranges):
|
||||
"""Find the best match for a mime-type amongst parsed media-ranges.
|
||||
|
||||
Find the best match for a given mime-type against a list of media_ranges
|
||||
that have already been parsed by parse_media_range(). Returns a tuple of
|
||||
the fitness value and the value of the 'q' quality parameter of the best
|
||||
match, or (-1, 0) if no match was found. Just as for quality_parsed(),
|
||||
'parsed_ranges' must be a list of parsed media ranges.
|
||||
"""
|
||||
best_fitness = -1
|
||||
best_fit_q = 0
|
||||
(target_type, target_subtype, target_params) = parse_media_range(mime_type)
|
||||
for (type, subtype, params) in parsed_ranges:
|
||||
type_match = type == target_type or type == "*" or target_type == "*"
|
||||
subtype_match = (
|
||||
subtype == target_subtype or subtype == "*" or target_subtype == "*"
|
||||
)
|
||||
if type_match and subtype_match:
|
||||
param_matches = reduce(
|
||||
lambda x, y: x + y,
|
||||
[
|
||||
1
|
||||
for (key, value) in target_params.items()
|
||||
if key != "q" and key in params and value == params[key]
|
||||
],
|
||||
0,
|
||||
)
|
||||
fitness = (type == target_type) and 100 or 0
|
||||
fitness += (subtype == target_subtype) and 10 or 0
|
||||
fitness += param_matches
|
||||
if fitness > best_fitness:
|
||||
best_fitness = fitness
|
||||
best_fit_q = params["q"]
|
||||
|
||||
return best_fitness, float(best_fit_q)
|
||||
|
||||
|
||||
def quality_parsed(mime_type, parsed_ranges):
|
||||
"""Find the best match for a mime-type amongst parsed media-ranges.
|
||||
|
||||
Find the best match for a given mime-type against a list of media_ranges
|
||||
that have already been parsed by parse_media_range(). Returns the 'q'
|
||||
quality parameter of the best match, 0 if no match was found. This function
|
||||
bahaves the same as quality() except that 'parsed_ranges' must be a list of
|
||||
parsed media ranges.
|
||||
"""
|
||||
|
||||
return fitness_and_quality_parsed(mime_type, parsed_ranges)[1]
|
||||
|
||||
|
||||
def quality(mime_type, ranges):
|
||||
"""Return the quality ('q') of a mime-type against a list of media-ranges.
|
||||
|
||||
Returns the quality 'q' of a mime-type when compared against the
|
||||
media-ranges in ranges. For example:
|
||||
|
||||
>>> quality('text/html','text/*;q=0.3, text/html;q=0.7,
|
||||
text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
|
||||
0.7
|
||||
|
||||
"""
|
||||
parsed_ranges = [parse_media_range(r) for r in ranges.split(",")]
|
||||
|
||||
return quality_parsed(mime_type, parsed_ranges)
|
||||
|
||||
|
||||
def best_match(supported, header):
|
||||
"""Return mime-type with the highest quality ('q') from list of candidates.
|
||||
|
||||
Takes a list of supported mime-types and finds the best match for all the
|
||||
media-ranges listed in header. The value of header must be a string that
|
||||
conforms to the format of the HTTP Accept: header. The value of 'supported'
|
||||
is a list of mime-types. The list of supported mime-types should be sorted
|
||||
in order of increasing desirability, in case of a situation where there is
|
||||
a tie.
|
||||
|
||||
>>> best_match(['application/xbel+xml', 'text/xml'],
|
||||
'text/*;q=0.5,*/*; q=0.1')
|
||||
'text/xml'
|
||||
"""
|
||||
split_header = _filter_blank(header.split(","))
|
||||
parsed_header = [parse_media_range(r) for r in split_header]
|
||||
weighted_matches = []
|
||||
pos = 0
|
||||
for mime_type in supported:
|
||||
weighted_matches.append(
|
||||
(fitness_and_quality_parsed(mime_type, parsed_header), pos, mime_type)
|
||||
)
|
||||
pos += 1
|
||||
weighted_matches.sort()
|
||||
|
||||
return weighted_matches[-1][0][1] and weighted_matches[-1][2] or ""
|
||||
|
||||
|
||||
def _filter_blank(i):
|
||||
for s in i:
|
||||
if s.strip():
|
||||
yield s
|
||||
@@ -1,429 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Model objects for requests and responses.
|
||||
|
||||
Each API may support one or more serializations, such
|
||||
as JSON, Atom, etc. The model classes are responsible
|
||||
for converting between the wire format and the Python
|
||||
object representation.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "jcgregorio@google.com (Joe Gregorio)"
|
||||
|
||||
import json
|
||||
import logging
|
||||
import platform
|
||||
import urllib
|
||||
import warnings
|
||||
|
||||
from googleapiclient import version as googleapiclient_version
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
try:
|
||||
from google.api_core.version_header import API_VERSION_METADATA_KEY
|
||||
|
||||
HAS_API_VERSION = True
|
||||
except ImportError:
|
||||
HAS_API_VERSION = False
|
||||
|
||||
_LIBRARY_VERSION = googleapiclient_version.__version__
|
||||
_PY_VERSION = platform.python_version()
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
dump_request_response = False
|
||||
|
||||
|
||||
def _abstract():
|
||||
raise NotImplementedError("You need to override this function")
|
||||
|
||||
|
||||
class Model(object):
|
||||
"""Model base class.
|
||||
|
||||
All Model classes should implement this interface.
|
||||
The Model serializes and de-serializes between a wire
|
||||
format such as JSON and a Python object representation.
|
||||
"""
|
||||
|
||||
def request(self, headers, path_params, query_params, body_value):
|
||||
"""Updates outgoing requests with a serialized body.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query_params: dict, parameters that appear in the query
|
||||
body_value: object, the request body as a Python object, which must be
|
||||
serializable.
|
||||
Returns:
|
||||
A tuple of (headers, path_params, query, body)
|
||||
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query: string, query part of the request URI
|
||||
body: string, the body serialized in the desired wire format.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
def response(self, resp, content):
|
||||
"""Convert the response wire format into a Python object.
|
||||
|
||||
Args:
|
||||
resp: httplib2.Response, the HTTP response headers and status
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
|
||||
Raises:
|
||||
googleapiclient.errors.HttpError if a non 2xx response is received.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
"""Base model class.
|
||||
|
||||
Subclasses should provide implementations for the "serialize" and
|
||||
"deserialize" methods, as well as values for the following class attributes.
|
||||
|
||||
Attributes:
|
||||
accept: The value to use for the HTTP Accept header.
|
||||
content_type: The value to use for the HTTP Content-type header.
|
||||
no_content_response: The value to return when deserializing a 204 "No
|
||||
Content" response.
|
||||
alt_param: The value to supply as the "alt" query parameter for requests.
|
||||
"""
|
||||
|
||||
accept = None
|
||||
content_type = None
|
||||
no_content_response = None
|
||||
alt_param = None
|
||||
|
||||
def _log_request(self, headers, path_params, query, body):
|
||||
"""Logs debugging information about the request if requested."""
|
||||
if dump_request_response:
|
||||
LOGGER.info("--request-start--")
|
||||
LOGGER.info("-headers-start-")
|
||||
for h, v in headers.items():
|
||||
LOGGER.info("%s: %s", h, v)
|
||||
LOGGER.info("-headers-end-")
|
||||
LOGGER.info("-path-parameters-start-")
|
||||
for h, v in path_params.items():
|
||||
LOGGER.info("%s: %s", h, v)
|
||||
LOGGER.info("-path-parameters-end-")
|
||||
LOGGER.info("body: %s", body)
|
||||
LOGGER.info("query: %s", query)
|
||||
LOGGER.info("--request-end--")
|
||||
|
||||
def request(self, headers, path_params, query_params, body_value, api_version=None):
|
||||
"""Updates outgoing requests with a serialized body.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query_params: dict, parameters that appear in the query
|
||||
body_value: object, the request body as a Python object, which must be
|
||||
serializable by json.
|
||||
api_version: str, The precise API version represented by this request,
|
||||
which will result in an API Version header being sent along with the
|
||||
HTTP request.
|
||||
Returns:
|
||||
A tuple of (headers, path_params, query, body)
|
||||
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query: string, query part of the request URI
|
||||
body: string, the body serialized as JSON
|
||||
"""
|
||||
query = self._build_query(query_params)
|
||||
headers["accept"] = self.accept
|
||||
headers["accept-encoding"] = "gzip, deflate"
|
||||
if "user-agent" in headers:
|
||||
headers["user-agent"] += " "
|
||||
else:
|
||||
headers["user-agent"] = ""
|
||||
headers["user-agent"] += "(gzip)"
|
||||
if "x-goog-api-client" in headers:
|
||||
headers["x-goog-api-client"] += " "
|
||||
else:
|
||||
headers["x-goog-api-client"] = ""
|
||||
headers["x-goog-api-client"] += "gdcl/%s gl-python/%s" % (
|
||||
_LIBRARY_VERSION,
|
||||
_PY_VERSION,
|
||||
)
|
||||
|
||||
if api_version and HAS_API_VERSION:
|
||||
headers[API_VERSION_METADATA_KEY] = api_version
|
||||
elif api_version:
|
||||
warnings.warn(
|
||||
"The `api_version` argument is ignored as a newer version of "
|
||||
"`google-api-core` is required to use this feature."
|
||||
"Please upgrade `google-api-core` to 2.19.0 or newer."
|
||||
)
|
||||
|
||||
if body_value is not None:
|
||||
headers["content-type"] = self.content_type
|
||||
body_value = self.serialize(body_value)
|
||||
self._log_request(headers, path_params, query, body_value)
|
||||
return (headers, path_params, query, body_value)
|
||||
|
||||
def _build_query(self, params):
|
||||
"""Builds a query string.
|
||||
|
||||
Args:
|
||||
params: dict, the query parameters
|
||||
|
||||
Returns:
|
||||
The query parameters properly encoded into an HTTP URI query string.
|
||||
"""
|
||||
if self.alt_param is not None:
|
||||
params.update({"alt": self.alt_param})
|
||||
astuples = []
|
||||
for key, value in params.items():
|
||||
if type(value) == type([]):
|
||||
for x in value:
|
||||
x = x.encode("utf-8")
|
||||
astuples.append((key, x))
|
||||
else:
|
||||
if isinstance(value, str) and callable(value.encode):
|
||||
value = value.encode("utf-8")
|
||||
astuples.append((key, value))
|
||||
return "?" + urllib.parse.urlencode(astuples)
|
||||
|
||||
def _log_response(self, resp, content):
|
||||
"""Logs debugging information about the response if requested."""
|
||||
if dump_request_response:
|
||||
LOGGER.info("--response-start--")
|
||||
for h, v in resp.items():
|
||||
LOGGER.info("%s: %s", h, v)
|
||||
if content:
|
||||
LOGGER.info(content)
|
||||
LOGGER.info("--response-end--")
|
||||
|
||||
def response(self, resp, content):
|
||||
"""Convert the response wire format into a Python object.
|
||||
|
||||
Args:
|
||||
resp: httplib2.Response, the HTTP response headers and status
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
|
||||
Raises:
|
||||
googleapiclient.errors.HttpError if a non 2xx response is received.
|
||||
"""
|
||||
self._log_response(resp, content)
|
||||
# Error handling is TBD, for example, do we retry
|
||||
# for some operation/error combinations?
|
||||
if resp.status < 300:
|
||||
if resp.status == 204:
|
||||
# A 204: No Content response should be treated differently
|
||||
# to all the other success states
|
||||
return self.no_content_response
|
||||
return self.deserialize(content)
|
||||
else:
|
||||
LOGGER.debug("Content from bad request was: %r" % content)
|
||||
raise HttpError(resp, content)
|
||||
|
||||
def serialize(self, body_value):
|
||||
"""Perform the actual Python object serialization.
|
||||
|
||||
Args:
|
||||
body_value: object, the request body as a Python object.
|
||||
|
||||
Returns:
|
||||
string, the body in serialized form.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
def deserialize(self, content):
|
||||
"""Perform the actual deserialization from response string to Python
|
||||
object.
|
||||
|
||||
Args:
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
|
||||
class JsonModel(BaseModel):
|
||||
"""Model class for JSON.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request and response bodies.
|
||||
"""
|
||||
|
||||
accept = "application/json"
|
||||
content_type = "application/json"
|
||||
alt_param = "json"
|
||||
|
||||
def __init__(self, data_wrapper=False):
|
||||
"""Construct a JsonModel.
|
||||
|
||||
Args:
|
||||
data_wrapper: boolean, wrap requests and responses in a data wrapper
|
||||
"""
|
||||
self._data_wrapper = data_wrapper
|
||||
|
||||
def serialize(self, body_value):
|
||||
if (
|
||||
isinstance(body_value, dict)
|
||||
and "data" not in body_value
|
||||
and self._data_wrapper
|
||||
):
|
||||
body_value = {"data": body_value}
|
||||
return json.dumps(body_value)
|
||||
|
||||
def deserialize(self, content):
|
||||
try:
|
||||
content = content.decode("utf-8")
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
body = json.loads(content)
|
||||
except json.decoder.JSONDecodeError:
|
||||
body = content
|
||||
else:
|
||||
if self._data_wrapper and "data" in body:
|
||||
body = body["data"]
|
||||
return body
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RawModel(JsonModel):
|
||||
"""Model class for requests that don't return JSON.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request, and returns the raw bytes
|
||||
of the response body.
|
||||
"""
|
||||
|
||||
accept = "*/*"
|
||||
content_type = "application/json"
|
||||
alt_param = None
|
||||
|
||||
def deserialize(self, content):
|
||||
return content
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return ""
|
||||
|
||||
|
||||
class MediaModel(JsonModel):
|
||||
"""Model class for requests that return Media.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request, and returns the raw bytes
|
||||
of the response body.
|
||||
"""
|
||||
|
||||
accept = "*/*"
|
||||
content_type = "application/json"
|
||||
alt_param = "media"
|
||||
|
||||
def deserialize(self, content):
|
||||
return content
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return ""
|
||||
|
||||
|
||||
class ProtocolBufferModel(BaseModel):
|
||||
"""Model class for protocol buffers.
|
||||
|
||||
Serializes and de-serializes the binary protocol buffer sent in the HTTP
|
||||
request and response bodies.
|
||||
"""
|
||||
|
||||
accept = "application/x-protobuf"
|
||||
content_type = "application/x-protobuf"
|
||||
alt_param = "proto"
|
||||
|
||||
def __init__(self, protocol_buffer):
|
||||
"""Constructs a ProtocolBufferModel.
|
||||
|
||||
The serialized protocol buffer returned in an HTTP response will be
|
||||
de-serialized using the given protocol buffer class.
|
||||
|
||||
Args:
|
||||
protocol_buffer: The protocol buffer class used to de-serialize a
|
||||
response from the API.
|
||||
"""
|
||||
self._protocol_buffer = protocol_buffer
|
||||
|
||||
def serialize(self, body_value):
|
||||
return body_value.SerializeToString()
|
||||
|
||||
def deserialize(self, content):
|
||||
return self._protocol_buffer.FromString(content)
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return self._protocol_buffer()
|
||||
|
||||
|
||||
def makepatch(original, modified):
|
||||
"""Create a patch object.
|
||||
|
||||
Some methods support PATCH, an efficient way to send updates to a resource.
|
||||
This method allows the easy construction of patch bodies by looking at the
|
||||
differences between a resource before and after it was modified.
|
||||
|
||||
Args:
|
||||
original: object, the original deserialized resource
|
||||
modified: object, the modified deserialized resource
|
||||
Returns:
|
||||
An object that contains only the changes from original to modified, in a
|
||||
form suitable to pass to a PATCH method.
|
||||
|
||||
Example usage:
|
||||
item = service.activities().get(postid=postid, userid=userid).execute()
|
||||
original = copy.deepcopy(item)
|
||||
item['object']['content'] = 'This is updated.'
|
||||
service.activities.patch(postid=postid, userid=userid,
|
||||
body=makepatch(original, item)).execute()
|
||||
"""
|
||||
patch = {}
|
||||
for key, original_value in original.items():
|
||||
modified_value = modified.get(key, None)
|
||||
if modified_value is None:
|
||||
# Use None to signal that the element is deleted
|
||||
patch[key] = None
|
||||
elif original_value != modified_value:
|
||||
if type(original_value) == type({}):
|
||||
# Recursively descend objects
|
||||
patch[key] = makepatch(original_value, modified_value)
|
||||
else:
|
||||
# In the case of simple types or arrays we just replace
|
||||
patch[key] = modified_value
|
||||
else:
|
||||
# Don't add anything to patch if there's no change
|
||||
pass
|
||||
for key in modified:
|
||||
if key not in original:
|
||||
patch[key] = modified[key]
|
||||
|
||||
return patch
|
||||
@@ -1,317 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Schema processing for discovery based APIs
|
||||
|
||||
Schemas holds an APIs discovery schemas. It can return those schema as
|
||||
deserialized JSON objects, or pretty print them as prototype objects that
|
||||
conform to the schema.
|
||||
|
||||
For example, given the schema:
|
||||
|
||||
schema = \"\"\"{
|
||||
"Foo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"etag": {
|
||||
"type": "string",
|
||||
"description": "ETag of the collection."
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "Type of the collection ('calendar#acl').",
|
||||
"default": "calendar#acl"
|
||||
},
|
||||
"nextPageToken": {
|
||||
"type": "string",
|
||||
"description": "Token used to access the next
|
||||
page of this result. Omitted if no further results are available."
|
||||
}
|
||||
}
|
||||
}
|
||||
}\"\"\"
|
||||
|
||||
s = Schemas(schema)
|
||||
print s.prettyPrintByName('Foo')
|
||||
|
||||
Produces the following output:
|
||||
|
||||
{
|
||||
"nextPageToken": "A String", # Token used to access the
|
||||
# next page of this result. Omitted if no further results are available.
|
||||
"kind": "A String", # Type of the collection ('calendar#acl').
|
||||
"etag": "A String", # ETag of the collection.
|
||||
},
|
||||
|
||||
The constructor takes a discovery document in which to look up named schema.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
# TODO(jcgregorio) support format, enum, minimum, maximum
|
||||
|
||||
__author__ = "jcgregorio@google.com (Joe Gregorio)"
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from googleapiclient import _helpers as util
|
||||
|
||||
|
||||
class Schemas(object):
|
||||
"""Schemas for an API."""
|
||||
|
||||
def __init__(self, discovery):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
discovery: object, Deserialized discovery document from which we pull
|
||||
out the named schema.
|
||||
"""
|
||||
self.schemas = discovery.get("schemas", {})
|
||||
|
||||
# Cache of pretty printed schemas.
|
||||
self.pretty = {}
|
||||
|
||||
@util.positional(2)
|
||||
def _prettyPrintByName(self, name, seen=None, dent=0):
|
||||
"""Get pretty printed object prototype from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Name of schema in the discovery document.
|
||||
seen: list of string, Names of schema already seen. Used to handle
|
||||
recursive definitions.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
if seen is None:
|
||||
seen = []
|
||||
|
||||
if name in seen:
|
||||
# Do not fall into an infinite loop over recursive definitions.
|
||||
return "# Object with schema name: %s" % name
|
||||
seen.append(name)
|
||||
|
||||
if name not in self.pretty:
|
||||
self.pretty[name] = _SchemaToStruct(
|
||||
self.schemas[name], seen, dent=dent
|
||||
).to_str(self._prettyPrintByName)
|
||||
|
||||
seen.pop()
|
||||
|
||||
return self.pretty[name]
|
||||
|
||||
def prettyPrintByName(self, name):
|
||||
"""Get pretty printed object prototype from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Name of schema in the discovery document.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
# Return with trailing comma and newline removed.
|
||||
return self._prettyPrintByName(name, seen=[], dent=0)[:-2]
|
||||
|
||||
@util.positional(2)
|
||||
def _prettyPrintSchema(self, schema, seen=None, dent=0):
|
||||
"""Get pretty printed object prototype of schema.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
seen: list of string, Names of schema already seen. Used to handle
|
||||
recursive definitions.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
if seen is None:
|
||||
seen = []
|
||||
|
||||
return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
|
||||
|
||||
def prettyPrintSchema(self, schema):
|
||||
"""Get pretty printed object prototype of schema.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
# Return with trailing comma and newline removed.
|
||||
return self._prettyPrintSchema(schema, dent=0)[:-2]
|
||||
|
||||
def get(self, name, default=None):
|
||||
"""Get deserialized JSON schema from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Schema name.
|
||||
default: object, return value if name not found.
|
||||
"""
|
||||
return self.schemas.get(name, default)
|
||||
|
||||
|
||||
class _SchemaToStruct(object):
|
||||
"""Convert schema to a prototype object."""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, schema, seen, dent=0):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
seen: list, List of names of schema already seen while parsing. Used to
|
||||
handle recursive definitions.
|
||||
dent: int, Initial indentation depth.
|
||||
"""
|
||||
# The result of this parsing kept as list of strings.
|
||||
self.value = []
|
||||
|
||||
# The final value of the parsing.
|
||||
self.string = None
|
||||
|
||||
# The parsed JSON schema.
|
||||
self.schema = schema
|
||||
|
||||
# Indentation level.
|
||||
self.dent = dent
|
||||
|
||||
# Method that when called returns a prototype object for the schema with
|
||||
# the given name.
|
||||
self.from_cache = None
|
||||
|
||||
# List of names of schema already seen while parsing.
|
||||
self.seen = seen
|
||||
|
||||
def emit(self, text):
|
||||
"""Add text as a line to the output.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
"""
|
||||
self.value.extend([" " * self.dent, text, "\n"])
|
||||
|
||||
def emitBegin(self, text):
|
||||
"""Add text to the output, but with no line terminator.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
"""
|
||||
self.value.extend([" " * self.dent, text])
|
||||
|
||||
def emitEnd(self, text, comment):
|
||||
"""Add text and comment to the output with line terminator.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
comment: string, Python comment.
|
||||
"""
|
||||
if comment:
|
||||
divider = "\n" + " " * (self.dent + 2) + "# "
|
||||
lines = comment.splitlines()
|
||||
lines = [x.rstrip() for x in lines]
|
||||
comment = divider.join(lines)
|
||||
self.value.extend([text, " # ", comment, "\n"])
|
||||
else:
|
||||
self.value.extend([text, "\n"])
|
||||
|
||||
def indent(self):
|
||||
"""Increase indentation level."""
|
||||
self.dent += 1
|
||||
|
||||
def undent(self):
|
||||
"""Decrease indentation level."""
|
||||
self.dent -= 1
|
||||
|
||||
def _to_str_impl(self, schema):
|
||||
"""Prototype object based on the schema, in Python code with comments.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema file.
|
||||
|
||||
Returns:
|
||||
Prototype object based on the schema, in Python code with comments.
|
||||
"""
|
||||
stype = schema.get("type")
|
||||
if stype == "object":
|
||||
self.emitEnd("{", schema.get("description", ""))
|
||||
self.indent()
|
||||
if "properties" in schema:
|
||||
properties = schema.get("properties", {})
|
||||
sorted_properties = OrderedDict(sorted(properties.items()))
|
||||
for pname, pschema in sorted_properties.items():
|
||||
self.emitBegin('"%s": ' % pname)
|
||||
self._to_str_impl(pschema)
|
||||
elif "additionalProperties" in schema:
|
||||
self.emitBegin('"a_key": ')
|
||||
self._to_str_impl(schema["additionalProperties"])
|
||||
self.undent()
|
||||
self.emit("},")
|
||||
elif "$ref" in schema:
|
||||
schemaName = schema["$ref"]
|
||||
description = schema.get("description", "")
|
||||
s = self.from_cache(schemaName, seen=self.seen)
|
||||
parts = s.splitlines()
|
||||
self.emitEnd(parts[0], description)
|
||||
for line in parts[1:]:
|
||||
self.emit(line.rstrip())
|
||||
elif stype == "boolean":
|
||||
value = schema.get("default", "True or False")
|
||||
self.emitEnd("%s," % str(value), schema.get("description", ""))
|
||||
elif stype == "string":
|
||||
value = schema.get("default", "A String")
|
||||
self.emitEnd('"%s",' % str(value), schema.get("description", ""))
|
||||
elif stype == "integer":
|
||||
value = schema.get("default", "42")
|
||||
self.emitEnd("%s," % str(value), schema.get("description", ""))
|
||||
elif stype == "number":
|
||||
value = schema.get("default", "3.14")
|
||||
self.emitEnd("%s," % str(value), schema.get("description", ""))
|
||||
elif stype == "null":
|
||||
self.emitEnd("None,", schema.get("description", ""))
|
||||
elif stype == "any":
|
||||
self.emitEnd('"",', schema.get("description", ""))
|
||||
elif stype == "array":
|
||||
self.emitEnd("[", schema.get("description"))
|
||||
self.indent()
|
||||
self.emitBegin("")
|
||||
self._to_str_impl(schema["items"])
|
||||
self.undent()
|
||||
self.emit("],")
|
||||
else:
|
||||
self.emit("Unknown type! %s" % stype)
|
||||
self.emitEnd("", "")
|
||||
|
||||
self.string = "".join(self.value)
|
||||
return self.string
|
||||
|
||||
def to_str(self, from_cache):
|
||||
"""Prototype object based on the schema, in Python code with comments.
|
||||
|
||||
Args:
|
||||
from_cache: callable(name, seen), Callable that retrieves an object
|
||||
prototype for a schema with the given name. Seen is a list of schema
|
||||
names already seen as we recursively descend the schema definition.
|
||||
|
||||
Returns:
|
||||
Prototype object based on the schema, in Python code with comments.
|
||||
The lines of the code will all be properly indented.
|
||||
"""
|
||||
self.from_cache = from_cache
|
||||
return self._to_str_impl(self.schema)
|
||||
@@ -1,15 +0,0 @@
|
||||
# Copyright 2021 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__version__ = "2.179.0"
|
||||
Reference in New Issue
Block a user