gam7 7.19.3__py3-none-any.whl → 7.28.2__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.
gam/gamlib/yubikey.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- # Copyright (C) 2023 Ross Scroggs All Rights Reserved.
3
+ # Copyright (C) 2025 Ross Scroggs All Rights Reserved.
4
4
  #
5
5
  # All Rights Reserved.
6
6
  #
@@ -19,11 +19,20 @@
19
19
  """YubiKey"""
20
20
 
21
21
  import base64
22
- import datetime
23
22
  from secrets import SystemRandom
24
23
  import string
25
24
  import sys
26
25
 
26
+ import arrow
27
+
28
+ from gam import mplock
29
+
30
+ from gam import systemErrorExit
31
+ from gam import readStdin
32
+ from gam import writeStdout
33
+
34
+ from gam.gamlib import glmsgs as Msg
35
+
27
36
  from cryptography.hazmat.primitives import hashes, serialization
28
37
  from cryptography.hazmat.primitives.asymmetric import padding
29
38
  from smartcard.Exceptions import CardConnectionException
@@ -49,14 +58,6 @@ YUBIKEY_VALUE_ERROR_RC = 85
49
58
  YUBIKEY_MULTIPLE_CONNECTED_RC = 86
50
59
  YUBIKEY_NOT_FOUND_RC = 87
51
60
 
52
- from gam import mplock
53
-
54
- from gam import systemErrorExit
55
- from gam import readStdin
56
- from gam import writeStdout
57
-
58
- from gam.gamlib import glmsgs as Msg
59
-
60
61
  PIN_PUK_CHARS = string.ascii_letters+string.digits+string.punctuation
61
62
 
62
63
  class YubiKey():
@@ -155,8 +156,8 @@ class YubiKey():
155
156
  KEY_TYPE.RSA2048,
156
157
  PIN_POLICY.ALWAYS,
157
158
  TOUCH_POLICY.NEVER)
158
- now = datetime.datetime.utcnow()
159
- valid_to = now + datetime.timedelta(days=36500)
159
+ now = arrow.utcnow()
160
+ valid_to = now.shift(days=36500)
160
161
  subject = 'CN=GAM Created Key'
161
162
  piv.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
162
163
  piv.verify_pin(new_pin)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gam7
3
- Version: 7.19.3
3
+ Version: 7.28.2
4
4
  Summary: CLI tool to manage Google Workspace
5
5
  Project-URL: Homepage, https://github.com/GAM-team/GAM
6
6
  Project-URL: Issues, https://github.com/GAM-team/GAM/issues
@@ -17,7 +17,8 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
- Requires-Python: >=3.9
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: arrow>=1.3.0
21
22
  Requires-Dist: chardet>=5.2.0
22
23
  Requires-Dist: cryptography>=44.0.2
23
24
  Requires-Dist: distro; sys_platform == 'linux'
@@ -26,11 +27,11 @@ Requires-Dist: google-api-python-client>=2.167.0
26
27
  Requires-Dist: google-auth-httplib2>=0.2.0
27
28
  Requires-Dist: google-auth-oauthlib>=1.2.2
28
29
  Requires-Dist: google-auth>=2.39.0
29
- Requires-Dist: httplib2>=0.22.0
30
+ Requires-Dist: httplib2>=0.31.0
30
31
  Requires-Dist: lxml>=5.4.0
31
32
  Requires-Dist: passlib>=1.7.4
32
33
  Requires-Dist: pathvalidate>=3.2.3
33
- Requires-Dist: python-dateutil
34
+ Requires-Dist: pysocks>=1.7.1
34
35
  Provides-Extra: yubikey
35
36
  Requires-Dist: yubikey-manager>=5.6.1; extra == 'yubikey'
36
37
  Description-Content-Type: text/markdown
@@ -55,6 +56,11 @@ this will download GAM, install it and start setup.
55
56
 
56
57
  Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM.
57
58
 
59
+ ## Use your own Python
60
+ If you'd prefer to install GAM as a Python package you can install with pip:
61
+ ```
62
+ pip install gam7
63
+ ```
58
64
  # Documentation
59
65
 
60
66
  The GAM documentation is hosted in the [GitHub Wiki]
@@ -1,12 +1,11 @@
1
- gam/__init__.py,sha256=ngnkOfi7vZjdRs83cEbcphscUZiEoju6GlIDSBF9nVM,3584315
2
- gam/__main__.py,sha256=amz0-959ph6zkZKqjaar4n60yho-T37w6qWI36qx0CA,1049
1
+ gam/__init__.py,sha256=7JwnsTQbnk6etqIKl33q4PI2MASrBI1AWoptjyvOU8E,3634088
2
+ gam/__main__.py,sha256=VwEYS7a9vYQPbT6iLduMOoVUJ6p4R-HZgerZQmM1NpE,1307
3
3
  gam/cacerts.pem,sha256=DUsVo2XlFYwfkhe3gnxa-Km4Z4noz74hSApXwTT-nQE,44344
4
4
  gam/cbcm-v1.1beta1.json,sha256=xO5XloCQQULmPbFBx5bckdqmbLFQ7sJ2TImhE1ysDIY,19439
5
5
  gam/contactdelegation-v1.json,sha256=kCWykxSH7jLHglb-nWE_RFauxRXa_1eisWXXAqHu6Ws,7663
6
6
  gam/datastudio-v1.json,sha256=L_mQBN8oYmfWODGvgrwTF9d2i8rEZJmzqIxw_Yr83lI,15673
7
7
  gam/meet-v2beta.json,sha256=VUx2P9QMmmNK5tU4W9SYzmVhQzMoshP4VbvHSPoNdTI,50511
8
8
  gam/serviceaccountlookup-v1.json,sha256=NDe8BbviJo8_vlEOA7iobT5GSaBg6h-DjnkPwiAYlvs,4121
9
- gam/six.py,sha256=U4Z_yv534W5CNyjY9i8V1OXY2SjAny8y2L5vDLhhThM,34159
10
9
  gam/atom/__init__.py,sha256=pnPkqTZcRh1LniagGNTBDO9COzniXUgRQv_DNE3KPBw,52245
11
10
  gam/atom/auth.py,sha256=hvzDptCCSNRi1hUK89hFmYn5rct5glWJ6Ffzm0p43bw,1200
12
11
  gam/atom/client.py,sha256=6AQbkndS2BSpw_17cd8C2y2zSBa4hXwZpOTKYZDFEjY,8086
@@ -23,19 +22,19 @@ gam/atom/token_store.py,sha256=7E6Ecvxa86WCvl1pJAhv78jg9OxQv8pMtIUcPhZCq04,3803
23
22
  gam/atom/url.py,sha256=pxO1TlORxyKQTQ1bkBE1unFzjnv9c8LjJkm-UEORShY,4276
24
23
  gam/gamlib/__init__.py,sha256=z5mF-y0j8pm-YNFBaiuxB4M_GAUPG-cXWwrhYwrVReM,679
25
24
  gam/gamlib/glaction.py,sha256=1Il_HrChVnPkzZwiZs5au4mFQVtq4K1Z42uIuR6qdnI,9419
26
- gam/gamlib/glapi.py,sha256=u97M7Y2BeP3tYEEGYEz-9kY4fdV0fYkeqC3YN-sRwNc,36028
27
- gam/gamlib/glcfg.py,sha256=87OwNxPbZy9nZr9n-12RSqTYai3zsZNYtR5WBaia6Y0,27906
28
- gam/gamlib/glclargs.py,sha256=pgomQOKbiKOI5TfdC0hTT5BqGbQcikDMRfkUk6DEXrM,43881
29
- gam/gamlib/glentity.py,sha256=ZQTdjVtGe8soMlgcSHJIpLHjQ_UtUL9qfw8ux-C4kik,34855
25
+ gam/gamlib/glapi.py,sha256=xV-Wj3Zz_LPV7PqufP3gdVyAuLDCUU4UbXGqefrOy04,36627
26
+ gam/gamlib/glcfg.py,sha256=do_FR9m9m7Bmh2SgjLPk2pysU-wPHt7PlqPPjX90tpw,28684
27
+ gam/gamlib/glclargs.py,sha256=C_mMXmW8rDfG6W5pn7kYR1vqluulkIm6i04npVPO0UU,53355
28
+ gam/gamlib/glentity.py,sha256=bIpmwanOU8XxsNCZsD42r2o2sMOkIPHmIJNCQ04140Q,35478
30
29
  gam/gamlib/glgapi.py,sha256=pdBbwNtnCwFWxJGaP-_3hdTjSNoOCJF2yo76WdQOi1k,40426
31
30
  gam/gamlib/glgdata.py,sha256=weRppttWm6uRyqtBoGPKoHiNZ2h28nhfUV4J_mbCszY,2707
32
- gam/gamlib/glglobals.py,sha256=J0xcHggVrUBzHJ5GruenKV-qV1zPKcK2qWgAgN3i5Jw,9608
31
+ gam/gamlib/glglobals.py,sha256=U1eCXHOkWAtwVXG8-0HL4ZzQP0YcbeFlhJxbOa_x1QI,9804
33
32
  gam/gamlib/glindent.py,sha256=RfBa2LDfLIqPLL5vMfC689TCVmqn8xf-qulSzkiatrc,1228
34
- gam/gamlib/glmsgs.py,sha256=vephDvTNbv55f79QzZWEKDcBTXr8knrwDxvx-1TYM6M,33997
35
- gam/gamlib/glskus.py,sha256=e1u3zw1MGQjBgAFXqjrGWQl2d7eYpVlMYGpIKNwjskQ,15360
33
+ gam/gamlib/glmsgs.py,sha256=TkLg6p8o9elVIbe_4Kq0hac6qSM-qoIMuQvXoEn4Q_c,34254
34
+ gam/gamlib/glskus.py,sha256=29vlBLBJCL4u9GawCt3eNeZq9HQG3hGFZk9-EainNng,15384
36
35
  gam/gamlib/gluprop.py,sha256=IyPLCyvn7-NHTUenM71YPQPXRZXx6CB5q-GtJ-FYd1c,11461
37
- gam/gamlib/glverlibs.py,sha256=xoQXiwcE_-HVYKv-VYA8O0mazRsc9mN-_ysj1dAlMyc,992
38
- gam/gamlib/yubikey.py,sha256=-UC-3oue9qarYK3LT7YL6Gmqs7TMK8oz9_AoxdaG2FA,7925
36
+ gam/gamlib/glverlibs.py,sha256=mDphdXVN_dJlGUVGuLqOEA3yHBNBeqRV4DRoTX7Wl7A,982
37
+ gam/gamlib/yubikey.py,sha256=UNPaeSx0y6_v1RnShO1e9ZyVSHni6sGvOgcEwktlCeE,7896
39
38
  gam/gdata/__init__.py,sha256=uvjmSza2EdL7lGaoJ04-uXHGeYa0i1dBQHIetBybcUQ,29637
40
39
  gam/gdata/service.py,sha256=CuImJDRVcNMM1dfo7V6T0LrztzqTNrIraaLkHXpL0Tw,70045
41
40
  gam/gdata/urlfetch.py,sha256=yAglxcDC3sCp763P9dXOVwKMjrHvwKRoVSMv_G3Rt8E,9308
@@ -48,25 +47,8 @@ gam/gdata/apps/audit/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrK
48
47
  gam/gdata/apps/audit/service.py,sha256=Z1eueThcNeVUMWP1DRWc_DGHrJCiJI8W_xj6L-cqu-Q,9658
49
48
  gam/gdata/apps/contacts/__init__.py,sha256=Um6zgIkiahZns7yAEuC3pxHSMD8iciZ_EoynSLoYPfU,30463
50
49
  gam/gdata/apps/contacts/service.py,sha256=5lNb-Ii1Gyek6ePFji3kyoYtCBc8CuJTwagx2BL2o14,15684
51
- gam/googleapiclient/__init__.py,sha256=kFWxKShJalbnrCuV0klL7mjZaiXfPpjlqmyKyI8yfTU,904
52
- gam/googleapiclient/_auth.py,sha256=QttUwhmp7BmhW5CReaAOVGH6kEtITYmEHhVCVmsnDPs,5736
53
- gam/googleapiclient/_helpers.py,sha256=iNxILG6iNNFukr7lhaYfVqX4oFM5SERvcsU3JxeOo6I,6723
54
- gam/googleapiclient/channel.py,sha256=Fc4nxu-RxGkjY_STp9bwJfZaLan6VdX-DNniH-ANSuE,11054
55
- gam/googleapiclient/discovery.py,sha256=9TKz9stsr8Q9TPTjKG1I6yHoPH1ISRvh58q_NoX3mOM,66341
56
- gam/googleapiclient/errors.py,sha256=9h3uimcMcczBHZJFWAX_YDABzJeJugWB0jmj11rp-LI,5460
57
- gam/googleapiclient/http.py,sha256=ITE51oqDBqN1-AA5D-Tnlj3egGc_5O0V5xSzBw3UTKI,68241
58
- gam/googleapiclient/mimeparse.py,sha256=wwouQMCjppTocJtiQhkkTa27kocYwlFRALL2z11Xo1Y,6530
59
- gam/googleapiclient/model.py,sha256=NQDO1GhOGNVCJlSSCLOecdA11yf8RDXfSLFxYb3R7EE,14085
60
- gam/googleapiclient/schema.py,sha256=rR3u8WPQ_V8a7GCUsNuvtf6GxzwuMO0HaqsTBp3tnyM,10414
61
- gam/googleapiclient/version.py,sha256=vY1VaLgft_SIONbdSFPvHzipKt1agXbOX-Z-ZFbo9s0,599
62
- gam/googleapiclient/discovery_cache/__init__.py,sha256=ww_vl0vhVLuHSEdRTv3-gq6EDG--Ff7rILYHHFifnzc,2315
63
- gam/googleapiclient/discovery_cache/appengine_memcache.py,sha256=6T1pQj-toAhDwfgLuiggFGhxKNGw5y-NnLUzLIF_M4s,1657
64
- gam/googleapiclient/discovery_cache/base.py,sha256=yCDPtxnbNN-p5_9fzBacC6P3wcUPlaCQIy5v_dXTons,1389
65
- gam/googleapiclient/discovery_cache/file_cache.py,sha256=sim3Mg4HgRYo3vX75jvcKy_aV568EvIrtBfvfbw-044,4774
66
- gam/iso8601/__init__.py,sha256=Z2PsYbXgAH5a5xzUvgczCboPzqWpm65kRcIngCnhViU,1218
67
- gam/iso8601/iso8601.py,sha256=Li2FHZ4sBTWuthuQhyCvmvj0j6At8JbGzkSv2fc2RHU,4384
68
- gam7-7.19.3.dist-info/METADATA,sha256=TeUdQCPXnItSyqXwJbMSkU3f43C-2H1wKRyc2AF7YPM,2940
69
- gam7-7.19.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
70
- gam7-7.19.3.dist-info/entry_points.txt,sha256=HVUM5J7dA8YwvJfG30jiLefR19ExMs387TWugWd9sf4,42
71
- gam7-7.19.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
72
- gam7-7.19.3.dist-info/RECORD,,
50
+ gam7-7.28.2.dist-info/METADATA,sha256=9T2F_6RdE6qx4VtSEF83_Dqh399tSvoKDARS-AEXXQ4,3093
51
+ gam7-7.28.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
52
+ gam7-7.28.2.dist-info/entry_points.txt,sha256=HVUM5J7dA8YwvJfG30jiLefR19ExMs387TWugWd9sf4,42
53
+ gam7-7.28.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
54
+ gam7-7.28.2.dist-info/RECORD,,
@@ -1,27 +0,0 @@
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())
@@ -1,167 +0,0 @@
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
@@ -1,207 +0,0 @@
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})