gam7 7.3.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of gam7 might be problematic. Click here for more details.

Files changed (72) hide show
  1. gam/__init__.py +77555 -0
  2. gam/__main__.py +40 -0
  3. gam/atom/__init__.py +1460 -0
  4. gam/atom/auth.py +41 -0
  5. gam/atom/client.py +214 -0
  6. gam/atom/core.py +535 -0
  7. gam/atom/data.py +327 -0
  8. gam/atom/http.py +354 -0
  9. gam/atom/http_core.py +599 -0
  10. gam/atom/http_interface.py +144 -0
  11. gam/atom/mock_http.py +123 -0
  12. gam/atom/mock_http_core.py +313 -0
  13. gam/atom/mock_service.py +235 -0
  14. gam/atom/service.py +723 -0
  15. gam/atom/token_store.py +105 -0
  16. gam/atom/url.py +130 -0
  17. gam/cacerts.pem +1130 -0
  18. gam/cbcm-v1.1beta1.json +593 -0
  19. gam/contactdelegation-v1.json +249 -0
  20. gam/datastudio-v1.json +486 -0
  21. gam/gamlib/__init__.py +17 -0
  22. gam/gamlib/glaction.py +308 -0
  23. gam/gamlib/glapi.py +837 -0
  24. gam/gamlib/glcfg.py +616 -0
  25. gam/gamlib/glclargs.py +1184 -0
  26. gam/gamlib/glentity.py +831 -0
  27. gam/gamlib/glgapi.py +817 -0
  28. gam/gamlib/glgdata.py +98 -0
  29. gam/gamlib/glglobals.py +307 -0
  30. gam/gamlib/glindent.py +46 -0
  31. gam/gamlib/glmsgs.py +547 -0
  32. gam/gamlib/glskus.py +246 -0
  33. gam/gamlib/gluprop.py +279 -0
  34. gam/gamlib/glverlibs.py +33 -0
  35. gam/gamlib/yubikey.py +202 -0
  36. gam/gdata/__init__.py +825 -0
  37. gam/gdata/alt/__init__.py +20 -0
  38. gam/gdata/alt/app_engine.py +101 -0
  39. gam/gdata/alt/appengine.py +321 -0
  40. gam/gdata/apps/__init__.py +526 -0
  41. gam/gdata/apps/audit/__init__.py +1 -0
  42. gam/gdata/apps/audit/service.py +278 -0
  43. gam/gdata/apps/contacts/__init__.py +874 -0
  44. gam/gdata/apps/contacts/service.py +355 -0
  45. gam/gdata/apps/service.py +544 -0
  46. gam/gdata/apps/sites/__init__.py +283 -0
  47. gam/gdata/apps/sites/service.py +246 -0
  48. gam/gdata/service.py +1714 -0
  49. gam/gdata/urlfetch.py +247 -0
  50. gam/googleapiclient/__init__.py +27 -0
  51. gam/googleapiclient/_auth.py +167 -0
  52. gam/googleapiclient/_helpers.py +207 -0
  53. gam/googleapiclient/channel.py +315 -0
  54. gam/googleapiclient/discovery.py +1662 -0
  55. gam/googleapiclient/discovery_cache/__init__.py +78 -0
  56. gam/googleapiclient/discovery_cache/appengine_memcache.py +55 -0
  57. gam/googleapiclient/discovery_cache/base.py +46 -0
  58. gam/googleapiclient/discovery_cache/file_cache.py +145 -0
  59. gam/googleapiclient/errors.py +197 -0
  60. gam/googleapiclient/http.py +1962 -0
  61. gam/googleapiclient/mimeparse.py +183 -0
  62. gam/googleapiclient/model.py +429 -0
  63. gam/googleapiclient/schema.py +317 -0
  64. gam/googleapiclient/version.py +15 -0
  65. gam/iso8601/__init__.py +28 -0
  66. gam/iso8601/iso8601.py +160 -0
  67. gam/serviceaccountlookup-v1.json +141 -0
  68. gam/six.py +982 -0
  69. gam7-7.3.4.dist-info/METADATA +69 -0
  70. gam7-7.3.4.dist-info/RECORD +72 -0
  71. gam7-7.3.4.dist-info/WHEEL +4 -0
  72. gam7-7.3.4.dist-info/licenses/LICENSE +201 -0
gam/gdata/urlfetch.py ADDED
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/python
2
+ #
3
+ # Copyright (C) 2008 Google Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+
18
+ """Provides HTTP functions for gdata.service to use on Google App Engine
19
+
20
+ AppEngineHttpClient: Provides an HTTP request method which uses App Engine's
21
+ urlfetch API. Set the http_client member of a GDataService object to an
22
+ instance of an AppEngineHttpClient to allow the gdata library to run on
23
+ Google App Engine.
24
+
25
+ run_on_appengine: Function which will modify an existing GDataService object
26
+ to allow it to run on App Engine. It works by creating a new instance of
27
+ the AppEngineHttpClient and replacing the GDataService object's
28
+ http_client.
29
+
30
+ HttpRequest: Function that wraps google.appengine.api.urlfetch.Fetch in a
31
+ common interface which is used by gdata.service.GDataService. In other
32
+ words, this module can be used as the gdata service request handler so
33
+ that all HTTP requests will be performed by the hosting Google App Engine
34
+ server.
35
+ """
36
+
37
+
38
+ __author__ = 'api.jscudder (Jeff Scudder)'
39
+
40
+
41
+ import io
42
+ import atom.service
43
+ import atom.http_interface
44
+ from google.appengine.api import urlfetch
45
+
46
+
47
+ def run_on_appengine(gdata_service):
48
+ """Modifies a GDataService object to allow it to run on App Engine.
49
+
50
+ Args:
51
+ gdata_service: An instance of AtomService, GDataService, or any
52
+ of their subclasses which has an http_client member.
53
+ """
54
+ gdata_service.http_client = AppEngineHttpClient()
55
+
56
+
57
+ class AppEngineHttpClient(atom.http_interface.GenericHttpClient):
58
+ def __init__(self, headers=None):
59
+ self.debug = False
60
+ self.headers = headers or {}
61
+
62
+ def request(self, operation, url, data=None, headers=None):
63
+ """Performs an HTTP call to the server, supports GET, POST, PUT, and
64
+ DELETE.
65
+
66
+ Usage example, perform and HTTP GET on http://www.google.com/:
67
+ import atom.http
68
+ client = atom.http.HttpClient()
69
+ http_response = client.request('GET', 'http://www.google.com/')
70
+
71
+ Args:
72
+ operation: str The HTTP operation to be performed. This is usually one
73
+ of 'GET', 'POST', 'PUT', or 'DELETE'
74
+ data: filestream, list of parts, or other object which can be converted
75
+ to a string. Should be set to None when performing a GET or DELETE.
76
+ If data is a file-like object which can be read, this method will
77
+ read a chunk of 100K bytes at a time and send them.
78
+ If the data is a list of parts to be sent, each part will be
79
+ evaluated and sent.
80
+ url: The full URL to which the request should be sent. Can be a string
81
+ or atom.url.Url.
82
+ headers: dict of strings. HTTP headers which should be sent
83
+ in the request.
84
+ """
85
+ all_headers = self.headers.copy()
86
+ if headers:
87
+ all_headers.update(headers)
88
+
89
+ # Construct the full payload.
90
+ # Assume that data is None or a string.
91
+ data_str = data
92
+ if data:
93
+ if isinstance(data, list):
94
+ # If data is a list of different objects, convert them all to strings
95
+ # and join them together.
96
+ converted_parts = [__ConvertDataPart(x) for x in data]
97
+ data_str = ''.join(converted_parts)
98
+ else:
99
+ data_str = __ConvertDataPart(data)
100
+
101
+ # If the list of headers does not include a Content-Length, attempt to
102
+ # calculate it based on the data object.
103
+ if data and 'Content-Length' not in all_headers:
104
+ all_headers['Content-Length'] = len(data_str)
105
+
106
+ # Set the content type to the default value if none was set.
107
+ if 'Content-Type' not in all_headers:
108
+ all_headers['Content-Type'] = 'application/atom+xml'
109
+
110
+ # Lookup the urlfetch operation which corresponds to the desired HTTP verb.
111
+ if operation == 'GET':
112
+ method = urlfetch.GET
113
+ elif operation == 'POST':
114
+ method = urlfetch.POST
115
+ elif operation == 'PUT':
116
+ method = urlfetch.PUT
117
+ elif operation == 'DELETE':
118
+ method = urlfetch.DELETE
119
+ else:
120
+ method = None
121
+ return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
122
+ method=method, headers=all_headers))
123
+
124
+
125
+ def HttpRequest(service, operation, data, uri, extra_headers=None,
126
+ url_params=None, escape_params=True, content_type='application/atom+xml'):
127
+ """Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
128
+
129
+ This function is deprecated, use AppEngineHttpClient.request instead.
130
+
131
+ To use this module with gdata.service, you can set this module to be the
132
+ http_request_handler so that HTTP requests use Google App Engine's urlfetch.
133
+ import gdata.service
134
+ import gdata.urlfetch
135
+ gdata.service.http_request_handler = gdata.urlfetch
136
+
137
+ Args:
138
+ service: atom.AtomService object which contains some of the parameters
139
+ needed to make the request. The following members are used to
140
+ construct the HTTP call: server (str), additional_headers (dict),
141
+ port (int), and ssl (bool).
142
+ operation: str The HTTP operation to be performed. This is usually one of
143
+ 'GET', 'POST', 'PUT', or 'DELETE'
144
+ data: filestream, list of parts, or other object which can be
145
+ converted to a string.
146
+ Should be set to None when performing a GET or PUT.
147
+ If data is a file-like object which can be read, this method will read
148
+ a chunk of 100K bytes at a time and send them.
149
+ If the data is a list of parts to be sent, each part will be evaluated
150
+ and sent.
151
+ uri: The beginning of the URL to which the request should be sent.
152
+ Examples: '/', '/base/feeds/snippets',
153
+ '/m8/feeds/contacts/default/base'
154
+ extra_headers: dict of strings. HTTP headers which should be sent
155
+ in the request. These headers are in addition to those stored in
156
+ service.additional_headers.
157
+ url_params: dict of strings. Key value pairs to be added to the URL as
158
+ URL parameters. For example {'foo':'bar', 'test':'param'} will
159
+ become ?foo=bar&test=param.
160
+ escape_params: bool default True. If true, the keys and values in
161
+ url_params will be URL escaped when the form is constructed
162
+ (Special characters converted to %XX form.)
163
+ content_type: str The MIME type for the data being sent. Defaults to
164
+ 'application/atom+xml', this is only used if data is set.
165
+ """
166
+ full_uri = atom.service.BuildUri(uri, url_params, escape_params)
167
+ (server, port, ssl, partial_uri) = atom.service.ProcessUrl(service, full_uri)
168
+ # Construct the full URL for the request.
169
+ if ssl:
170
+ full_url = 'https://%s%s' % (server, partial_uri)
171
+ else:
172
+ full_url = 'http://%s%s' % (server, partial_uri)
173
+
174
+ # Construct the full payload.
175
+ # Assume that data is None or a string.
176
+ data_str = data
177
+ if data:
178
+ if isinstance(data, list):
179
+ # If data is a list of different objects, convert them all to strings
180
+ # and join them together.
181
+ converted_parts = [__ConvertDataPart(x) for x in data]
182
+ data_str = ''.join(converted_parts)
183
+ else:
184
+ data_str = __ConvertDataPart(data)
185
+
186
+ # Construct the dictionary of HTTP headers.
187
+ headers = {}
188
+ if isinstance(service.additional_headers, dict):
189
+ headers = service.additional_headers.copy()
190
+ if isinstance(extra_headers, dict):
191
+ for header, value in extra_headers.items():
192
+ headers[header] = value
193
+ # Add the content type header (we don't need to calculate content length,
194
+ # since urlfetch.Fetch will calculate for us).
195
+ if content_type:
196
+ headers['Content-Type'] = content_type
197
+
198
+ # Lookup the urlfetch operation which corresponds to the desired HTTP verb.
199
+ if operation == 'GET':
200
+ method = urlfetch.GET
201
+ elif operation == 'POST':
202
+ method = urlfetch.POST
203
+ elif operation == 'PUT':
204
+ method = urlfetch.PUT
205
+ elif operation == 'DELETE':
206
+ method = urlfetch.DELETE
207
+ else:
208
+ method = None
209
+ return HttpResponse(urlfetch.Fetch(url=full_url, payload=data_str,
210
+ method=method, headers=headers))
211
+
212
+
213
+ def __ConvertDataPart(data):
214
+ if not data or isinstance(data, str):
215
+ return data
216
+ elif hasattr(data, 'read'):
217
+ # data is a file like object, so read it completely.
218
+ return data.read()
219
+ # The data object was not a file.
220
+ # Try to convert to a string and send the data.
221
+ return str(data)
222
+
223
+
224
+ class HttpResponse(object):
225
+ """Translates a urlfetch resoinse to look like an hhtplib resoinse.
226
+
227
+ Used to allow the resoinse from HttpRequest to be usable by gdata.service
228
+ methods.
229
+ """
230
+
231
+ def __init__(self, urlfetch_response):
232
+ self.body = io.StringIO(urlfetch_response.content)
233
+ self.headers = urlfetch_response.headers
234
+ self.status = urlfetch_response.status_code
235
+ self.reason = ''
236
+
237
+ def read(self, length=None):
238
+ if not length:
239
+ return self.body.read()
240
+ else:
241
+ return self.body.read(length)
242
+
243
+ def getheader(self, name):
244
+ if name not in self.headers:
245
+ return self.headers[name.lower()]
246
+ return self.headers[name]
247
+
@@ -0,0 +1,27 @@
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Set default logging handler to avoid "No handler found" warnings.
16
+ import logging
17
+
18
+ try: # Python 2.7+
19
+ from logging import NullHandler
20
+ except ImportError:
21
+
22
+ class NullHandler(logging.Handler):
23
+ def emit(self, record):
24
+ pass
25
+
26
+
27
+ logging.getLogger(__name__).addHandler(NullHandler())
@@ -0,0 +1,167 @@
1
+ # Copyright 2016 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Helpers for authentication using oauth2client or google-auth."""
16
+
17
+ import httplib2
18
+
19
+ try:
20
+ import google.auth
21
+ import google.auth.credentials
22
+
23
+ HAS_GOOGLE_AUTH = True
24
+ except ImportError: # pragma: NO COVER
25
+ HAS_GOOGLE_AUTH = False
26
+
27
+ try:
28
+ import google_auth_httplib2
29
+ except ImportError: # pragma: NO COVER
30
+ google_auth_httplib2 = None
31
+
32
+ try:
33
+ import oauth2client
34
+ import oauth2client.client
35
+
36
+ HAS_OAUTH2CLIENT = True
37
+ except ImportError: # pragma: NO COVER
38
+ HAS_OAUTH2CLIENT = False
39
+
40
+
41
+ def credentials_from_file(filename, scopes=None, quota_project_id=None):
42
+ """Returns credentials loaded from a file."""
43
+ if HAS_GOOGLE_AUTH:
44
+ credentials, _ = google.auth.load_credentials_from_file(
45
+ filename, scopes=scopes, quota_project_id=quota_project_id
46
+ )
47
+ return credentials
48
+ else:
49
+ raise EnvironmentError(
50
+ "client_options.credentials_file is only supported in google-auth."
51
+ )
52
+
53
+
54
+ def default_credentials(scopes=None, quota_project_id=None):
55
+ """Returns Application Default Credentials."""
56
+ if HAS_GOOGLE_AUTH:
57
+ credentials, _ = google.auth.default(
58
+ scopes=scopes, quota_project_id=quota_project_id
59
+ )
60
+ return credentials
61
+ elif HAS_OAUTH2CLIENT:
62
+ if scopes is not None or quota_project_id is not None:
63
+ raise EnvironmentError(
64
+ "client_options.scopes and client_options.quota_project_id are not supported in oauth2client."
65
+ "Please install google-auth."
66
+ )
67
+ return oauth2client.client.GoogleCredentials.get_application_default()
68
+ else:
69
+ raise EnvironmentError(
70
+ "No authentication library is available. Please install either "
71
+ "google-auth or oauth2client."
72
+ )
73
+
74
+
75
+ def with_scopes(credentials, scopes):
76
+ """Scopes the credentials if necessary.
77
+
78
+ Args:
79
+ credentials (Union[
80
+ google.auth.credentials.Credentials,
81
+ oauth2client.client.Credentials]): The credentials to scope.
82
+ scopes (Sequence[str]): The list of scopes.
83
+
84
+ Returns:
85
+ Union[google.auth.credentials.Credentials,
86
+ oauth2client.client.Credentials]: The scoped credentials.
87
+ """
88
+ if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
89
+ return google.auth.credentials.with_scopes_if_required(credentials, scopes)
90
+ else:
91
+ try:
92
+ if credentials.create_scoped_required():
93
+ return credentials.create_scoped(scopes)
94
+ else:
95
+ return credentials
96
+ except AttributeError:
97
+ return credentials
98
+
99
+
100
+ def authorized_http(credentials):
101
+ """Returns an http client that is authorized with the given credentials.
102
+
103
+ Args:
104
+ credentials (Union[
105
+ google.auth.credentials.Credentials,
106
+ oauth2client.client.Credentials]): The credentials to use.
107
+
108
+ Returns:
109
+ Union[httplib2.Http, google_auth_httplib2.AuthorizedHttp]: An
110
+ authorized http client.
111
+ """
112
+ from googleapiclient.http import build_http
113
+
114
+ if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
115
+ if google_auth_httplib2 is None:
116
+ raise ValueError(
117
+ "Credentials from google.auth specified, but "
118
+ "google-api-python-client is unable to use these credentials "
119
+ "unless google-auth-httplib2 is installed. Please install "
120
+ "google-auth-httplib2."
121
+ )
122
+ return google_auth_httplib2.AuthorizedHttp(credentials, http=build_http())
123
+ else:
124
+ return credentials.authorize(build_http())
125
+
126
+
127
+ def refresh_credentials(credentials):
128
+ # Refresh must use a new http instance, as the one associated with the
129
+ # credentials could be a AuthorizedHttp or an oauth2client-decorated
130
+ # Http instance which would cause a weird recursive loop of refreshing
131
+ # and likely tear a hole in spacetime.
132
+ refresh_http = httplib2.Http()
133
+ if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
134
+ request = google_auth_httplib2.Request(refresh_http)
135
+ return credentials.refresh(request)
136
+ else:
137
+ return credentials.refresh(refresh_http)
138
+
139
+
140
+ def apply_credentials(credentials, headers):
141
+ # oauth2client and google-auth have the same interface for this.
142
+ if not is_valid(credentials):
143
+ refresh_credentials(credentials)
144
+ return credentials.apply(headers)
145
+
146
+
147
+ def is_valid(credentials):
148
+ if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
149
+ return credentials.valid
150
+ else:
151
+ return (
152
+ credentials.access_token is not None
153
+ and not credentials.access_token_expired
154
+ )
155
+
156
+
157
+ def get_credentials_from_http(http):
158
+ if http is None:
159
+ return None
160
+ elif hasattr(http.request, "credentials"):
161
+ return http.request.credentials
162
+ elif hasattr(http, "credentials") and not isinstance(
163
+ http.credentials, httplib2.Credentials
164
+ ):
165
+ return http.credentials
166
+ else:
167
+ return None
@@ -0,0 +1,207 @@
1
+ # Copyright 2015 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Helper functions for commonly used utilities."""
16
+
17
+ import functools
18
+ import inspect
19
+ import logging
20
+ import urllib
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ POSITIONAL_WARNING = "WARNING"
25
+ POSITIONAL_EXCEPTION = "EXCEPTION"
26
+ POSITIONAL_IGNORE = "IGNORE"
27
+ POSITIONAL_SET = frozenset(
28
+ [POSITIONAL_WARNING, POSITIONAL_EXCEPTION, POSITIONAL_IGNORE]
29
+ )
30
+
31
+ positional_parameters_enforcement = POSITIONAL_WARNING
32
+
33
+ _SYM_LINK_MESSAGE = "File: {0}: Is a symbolic link."
34
+ _IS_DIR_MESSAGE = "{0}: Is a directory"
35
+ _MISSING_FILE_MESSAGE = "Cannot access {0}: No such file or directory"
36
+
37
+
38
+ def positional(max_positional_args):
39
+ """A decorator to declare that only the first N arguments may be positional.
40
+
41
+ This decorator makes it easy to support Python 3 style keyword-only
42
+ parameters. For example, in Python 3 it is possible to write::
43
+
44
+ def fn(pos1, *, kwonly1=None, kwonly2=None):
45
+ ...
46
+
47
+ All named parameters after ``*`` must be a keyword::
48
+
49
+ fn(10, 'kw1', 'kw2') # Raises exception.
50
+ fn(10, kwonly1='kw1') # Ok.
51
+
52
+ Example
53
+ ^^^^^^^
54
+
55
+ To define a function like above, do::
56
+
57
+ @positional(1)
58
+ def fn(pos1, kwonly1=None, kwonly2=None):
59
+ ...
60
+
61
+ If no default value is provided to a keyword argument, it becomes a
62
+ required keyword argument::
63
+
64
+ @positional(0)
65
+ def fn(required_kw):
66
+ ...
67
+
68
+ This must be called with the keyword parameter::
69
+
70
+ fn() # Raises exception.
71
+ fn(10) # Raises exception.
72
+ fn(required_kw=10) # Ok.
73
+
74
+ When defining instance or class methods always remember to account for
75
+ ``self`` and ``cls``::
76
+
77
+ class MyClass(object):
78
+
79
+ @positional(2)
80
+ def my_method(self, pos1, kwonly1=None):
81
+ ...
82
+
83
+ @classmethod
84
+ @positional(2)
85
+ def my_method(cls, pos1, kwonly1=None):
86
+ ...
87
+
88
+ The positional decorator behavior is controlled by
89
+ ``_helpers.positional_parameters_enforcement``, which may be set to
90
+ ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
91
+ ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
92
+ nothing, respectively, if a declaration is violated.
93
+
94
+ Args:
95
+ max_positional_arguments: Maximum number of positional arguments. All
96
+ parameters after this index must be
97
+ keyword only.
98
+
99
+ Returns:
100
+ A decorator that prevents using arguments after max_positional_args
101
+ from being used as positional parameters.
102
+
103
+ Raises:
104
+ TypeError: if a keyword-only argument is provided as a positional
105
+ parameter, but only if
106
+ _helpers.positional_parameters_enforcement is set to
107
+ POSITIONAL_EXCEPTION.
108
+ """
109
+
110
+ def positional_decorator(wrapped):
111
+ @functools.wraps(wrapped)
112
+ def positional_wrapper(*args, **kwargs):
113
+ if len(args) > max_positional_args:
114
+ plural_s = ""
115
+ if max_positional_args != 1:
116
+ plural_s = "s"
117
+ message = (
118
+ "{function}() takes at most {args_max} positional "
119
+ "argument{plural} ({args_given} given)".format(
120
+ function=wrapped.__name__,
121
+ args_max=max_positional_args,
122
+ args_given=len(args),
123
+ plural=plural_s,
124
+ )
125
+ )
126
+ if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
127
+ raise TypeError(message)
128
+ elif positional_parameters_enforcement == POSITIONAL_WARNING:
129
+ logger.warning(message)
130
+ return wrapped(*args, **kwargs)
131
+
132
+ return positional_wrapper
133
+
134
+ if isinstance(max_positional_args, int):
135
+ return positional_decorator
136
+ else:
137
+ args, _, _, defaults, _, _, _ = inspect.getfullargspec(max_positional_args)
138
+ return positional(len(args) - len(defaults))(max_positional_args)
139
+
140
+
141
+ def parse_unique_urlencoded(content):
142
+ """Parses unique key-value parameters from urlencoded content.
143
+
144
+ Args:
145
+ content: string, URL-encoded key-value pairs.
146
+
147
+ Returns:
148
+ dict, The key-value pairs from ``content``.
149
+
150
+ Raises:
151
+ ValueError: if one of the keys is repeated.
152
+ """
153
+ urlencoded_params = urllib.parse.parse_qs(content)
154
+ params = {}
155
+ for key, value in urlencoded_params.items():
156
+ if len(value) != 1:
157
+ msg = "URL-encoded content contains a repeated value:" "%s -> %s" % (
158
+ key,
159
+ ", ".join(value),
160
+ )
161
+ raise ValueError(msg)
162
+ params[key] = value[0]
163
+ return params
164
+
165
+
166
+ def update_query_params(uri, params):
167
+ """Updates a URI with new query parameters.
168
+
169
+ If a given key from ``params`` is repeated in the ``uri``, then
170
+ the URI will be considered invalid and an error will occur.
171
+
172
+ If the URI is valid, then each value from ``params`` will
173
+ replace the corresponding value in the query parameters (if
174
+ it exists).
175
+
176
+ Args:
177
+ uri: string, A valid URI, with potential existing query parameters.
178
+ params: dict, A dictionary of query parameters.
179
+
180
+ Returns:
181
+ The same URI but with the new query parameters added.
182
+ """
183
+ parts = urllib.parse.urlparse(uri)
184
+ query_params = parse_unique_urlencoded(parts.query)
185
+ query_params.update(params)
186
+ new_query = urllib.parse.urlencode(query_params)
187
+ new_parts = parts._replace(query=new_query)
188
+ return urllib.parse.urlunparse(new_parts)
189
+
190
+
191
+ def _add_query_parameter(url, name, value):
192
+ """Adds a query parameter to a url.
193
+
194
+ Replaces the current value if it already exists in the URL.
195
+
196
+ Args:
197
+ url: string, url to add the query parameter to.
198
+ name: string, query parameter name.
199
+ value: string, query parameter value.
200
+
201
+ Returns:
202
+ Updated query parameter. Does not update the url if value is None.
203
+ """
204
+ if value is None:
205
+ return url
206
+ else:
207
+ return update_query_params(url, {name: value})