Multiple changes

Added support for Duet AI license.

Added `api_call_tries_limit` variable to `gam.cfg` that limits the number of tries
for Google API calls that return an error that indicates a retry should be performed. The default value is 10 and the range of allowable values is 3-10.

Code cleanup for retry loops

Initial (not announced, in preview mode) code for Chat API support of group members and role management

Allow spaces/xxx and space/xxx when specifying chat spaces
This commit is contained in:
Ross Scroggs
2023-09-20 11:26:16 -07:00
parent 9999abe462
commit 38c78228aa
11 changed files with 191 additions and 97 deletions

View File

@ -197,11 +197,11 @@ gam config csv_output_row_drop_filter <RowValueFilterJSONList> ...
You optionally specify whether all or any value filters must match for the row to be excluded from the output. You optionally specify whether all or any value filters must match for the row to be excluded from the output.
* `csv_output_row_filter_drop_mode allmatch` - If all value filters match, the row is excluded from the output * `csv_output_row_drop_filter_mode allmatch` - If all value filters match, the row is excluded from the output
* `csv_output_row_filter_drop_mode anymatch` - If any value filter matches, the row is excluded from the output; this is the default * `csv_output_row_drop_filter_mode anymatch` - If any value filter matches, the row is excluded from the output; this is the default
``` ```
gam config csv_output_row_filter_drop_mode allmatch csv_output_row_drop_filter <RowValueFilterList> ... gam config csv_output_row_drop_filter_mode allmatch csv_output_row_drop_filter <RowValueFilterList> ...
gam config csv_output_row_filter_drop_mode allmatch csv_output_row_drop_filter <RowValueFilterJSONList> ... gam config csv_output_row_drop_filter_mode allmatch csv_output_row_drop_filter <RowValueFilterJSONList> ...
``` ```
### Matches ### Matches

View File

@ -10,6 +10,16 @@ Add the `-s` option to the end of the above commands to suppress creating the `g
See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Windows or other options, including manual installation. See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Windows or other options, including manual installation.
### 6.63.17
Added support for Duet AI license.
* ProductID - 101047
* SKUID - 101047001 | duetai
Added `api_call_tries_limit` variable to `gam.cfg` that limits the number of tries
for Google API calls that return an error that indicates a retry should be performed.
The default value is 10 and the range of allowable values is 3-10.
### 6.63.16 ### 6.63.16
Arguments `noinherit`, `blockinheritance` and `blockinheritance true` have been removed from the following Arguments `noinherit`, `blockinheritance` and `blockinheritance true` have been removed from the following

View File

@ -106,6 +106,7 @@ Section: DEFAULT
admin_email = '' admin_email = ''
api_calls_rate_check = false api_calls_rate_check = false
api_calls_rate_limit = 100 api_calls_rate_limit = 100
api_calls_tries_limit = 10
auto_batch_min = 0 auto_batch_min = 0
bail_on_internal_error_tries = 2 bail_on_internal_error_tries = 2
batch_size = 50 batch_size = 50
@ -333,7 +334,7 @@ writes the credentials into the file oauth2.txt.
admin@server:/Users/admin/bin/gamadv-xtd3$ rm -f /Users/admin/GAMConfig/oauth2.txt admin@server:/Users/admin/bin/gamadv-xtd3$ rm -f /Users/admin/GAMConfig/oauth2.txt
admin@server:/Users/admin/bin/gamadv-xtd3$ ./gam version admin@server:/Users/admin/bin/gamadv-xtd3$ ./gam version
WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found
GAMADV-XTD3 6.63.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.63.17 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.10.8 64-bit final Python 3.10.8 64-bit final
MacOS High Sierra 10.13.6 x86_64 MacOS High Sierra 10.13.6 x86_64
@ -534,6 +535,7 @@ Section: DEFAULT
admin_email = '' admin_email = ''
api_calls_rate_check = false api_calls_rate_check = false
api_calls_rate_limit = 100 api_calls_rate_limit = 100
api_calls_tries_limit = 10
auto_batch_min = 0 auto_batch_min = 0
bail_on_internal_error_tries = 2 bail_on_internal_error_tries = 2
batch_size = 50 batch_size = 50
@ -735,6 +737,7 @@ Section: DEFAULT
admin_email = '' admin_email = ''
api_calls_rate_check = false api_calls_rate_check = false
api_calls_rate_limit = 100 api_calls_rate_limit = 100
api_calls_tries_limit = 10
auto_batch_min = 0 auto_batch_min = 0
bail_on_internal_error_tries = 2 bail_on_internal_error_tries = 2
batch_size = 50 batch_size = 50
@ -981,7 +984,7 @@ writes the credentials into the file oauth2.txt.
C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt
C:\GAMADV-XTD3>gam version C:\GAMADV-XTD3>gam version
WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found
GAMADV-XTD3 6.63.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.63.17 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.11.5 64-bit final Python 3.11.5 64-bit final
Windows-10-10.0.17134 AMD64 Windows-10-10.0.17134 AMD64
@ -1182,6 +1185,7 @@ Section: DEFAULT
admin_email = '' admin_email = ''
api_calls_rate_check = false api_calls_rate_check = false
api_calls_rate_limit = 100 api_calls_rate_limit = 100
api_calls_tries_limit = 10
auto_batch_min = 0 auto_batch_min = 0
bail_on_internal_error_tries = 2 bail_on_internal_error_tries = 2
batch_size = 50 batch_size = 50

View File

@ -24,6 +24,7 @@
| Cloud Identity Free | 101001 | | Cloud Identity Free | 101001 |
| Cloud Identity Premium | 101005 | | Cloud Identity Premium | 101005 |
| Cloud Search | 101035 | | Cloud Search | 101035 |
| Duet AI | 101047 |
| Google Chrome Device Management | Google-Chrome-Device-Management | | Google Chrome Device Management | Google-Chrome-Device-Management |
| Google Drive Storage | Google-Drive-storage | | Google Drive Storage | Google-Drive-storage |
| Google Meet Global Dialing | 101036 | | Google Meet Global Dialing | 101036 |
@ -44,6 +45,7 @@
| Cloud Identity Free | 1010010001 | cloudidentity | | Cloud Identity Free | 1010010001 | cloudidentity |
| Cloud Identity Premium | 1010050001 | cloudidentitypremium | | Cloud Identity Premium | 1010050001 | cloudidentitypremium |
| Cloud Search | 1010350001 | cloudsearch | | Cloud Search | 1010350001 | cloudsearch |
| Duet AI | 1010470001 | duetai |
| G Suite Basic | Google-Apps-For-Business | gsuitebasic | | G Suite Basic | Google-Apps-For-Business | gsuitebasic |
| G Suite Business | Google-Apps-Unlimited | gsuitebusiness | | G Suite Business | Google-Apps-Unlimited | gsuitebusiness |
| G Suite Legacy | Google-Apps | standard | | G Suite Legacy | Google-Apps | standard |
@ -108,6 +110,7 @@
101038 | 101038 |
101039 | 101039 |
101040 | 101040 |
101047 |
Google-Apps | Google-Apps |
Google-Chrome-Device-Management | Google-Chrome-Device-Management |
Google-Drive-storage | Google-Drive-storage |
@ -134,6 +137,7 @@
cloudidentity | identity | 1010010001 | cloudidentity | identity | 1010010001 |
cloudidentitypremium | identitypremium | 1010050001 | cloudidentitypremium | identitypremium | 1010050001 |
cloudsearch | 1010350001 | cloudsearch | 1010350001 |
duetai | 101047001 |
gsuitebasic | gafb | gafw | basic | Google-Apps-For-Business | gsuitebasic | gafb | gafw | basic | Google-Apps-For-Business |
gsuitebusiness | gau | gsb | unlimited | Google-Apps-Unlimited | gsuitebusiness | gau | gsb | unlimited | Google-Apps-Unlimited |
gsuitebusinessarchived | gsbau | businessarchived | 1010340002 | gsuitebusinessarchived | gsbau | businessarchived | 1010340002 |

View File

@ -3,7 +3,7 @@
Print the current version of Gam with details Print the current version of Gam with details
``` ```
gam version gam version
GAMADV-XTD3 6.63.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.63.17 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.11.5 64-bit final Python 3.11.5 64-bit final
MacOS Monterey 12.6.6 x86_64 MacOS Monterey 12.6.6 x86_64
@ -15,7 +15,7 @@ Time: 2023-06-02T21:10:00-07:00
Print the current version of Gam with details and time offset information Print the current version of Gam with details and time offset information
``` ```
gam version timeoffset gam version timeoffset
GAMADV-XTD3 6.63.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.63.17 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.11.5 64-bit final Python 3.11.5 64-bit final
MacOS Monterey 12.6.6 x86_64 MacOS Monterey 12.6.6 x86_64
@ -27,7 +27,7 @@ Your system time differs from www.googleapis.com by less than 1 second
Print the current version of Gam with extended details and SSL information Print the current version of Gam with extended details and SSL information
``` ```
gam version extended gam version extended
GAMADV-XTD3 6.63.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.63.17 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.11.5 64-bit final Python 3.11.5 64-bit final
MacOS Monterey 12.6.6 x86_64 MacOS Monterey 12.6.6 x86_64
@ -64,7 +64,7 @@ MacOS High Sierra 10.13.6 x86_64
Path: /Users/Admin/bin/gamadv-xtd3 Path: /Users/Admin/bin/gamadv-xtd3
Version Check: Version Check:
Current: 5.35.08 Current: 5.35.08
Latest: 6.63.16 Latest: 6.63.17
echo $? echo $?
1 1
``` ```
@ -72,7 +72,7 @@ echo $?
Print the current version number without details Print the current version number without details
``` ```
gam version simple gam version simple
6.63.16 6.63.17
``` ```
In Linux/MacOS you can do: In Linux/MacOS you can do:
``` ```
@ -82,7 +82,7 @@ echo $VER
Print the current version of Gam and address of this Wiki Print the current version of Gam and address of this Wiki
``` ```
gam help gam help
GAM 6.63.16 - https://github.com/taers232c/GAMADV-XTD3 GAM 6.63.17 - https://github.com/taers232c/GAMADV-XTD3
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.11.5 64-bit final Python 3.11.5 64-bit final
MacOS Monterey 12.6.6 x86_64 MacOS Monterey 12.6.6 x86_64

View File

@ -61,6 +61,11 @@ api_calls_rate_limit
Limit on number of Google API calls per 60 seconds Limit on number of Google API calls per 60 seconds
Default: 1000 Default: 1000
Range: 100 - Unlimited Range: 100 - Unlimited
api_calls_tries_limit
Limit the number of tries for Google API calls that return an error
that indicates a retry should be performed
Default: 10
Range: 3-10
auto_batch_min auto_batch_min
Automatically generate gam batch command if number of users Automatically generate gam batch command if number of users
specified in gam users xxx command exceeds this number specified in gam users xxx command exceeds this number
@ -582,6 +587,7 @@ Section: DEFAULT
admin_email = '' admin_email = ''
api_calls_rate_check = false api_calls_rate_check = false
api_calls_rate_limit = 100 api_calls_rate_limit = 100
api_calls_tries_limit = 10
auto_batch_min = 0 auto_batch_min = 0
bail_on_internal_error_tries = 2 bail_on_internal_error_tries = 2
batch_size = 50 batch_size = 50
@ -773,6 +779,7 @@ activity_max_results = 100
admin_email = '' admin_email = ''
api_calls_rate_check = false api_calls_rate_check = false
api_calls_rate_limit = 1000 api_calls_rate_limit = 1000
api_calls_tries_limit = 10
auto_batch_min = 0 auto_batch_min = 0
bail_on_internal_error_tries = 2 bail_on_internal_error_tries = 2
batch_size = 50 batch_size = 50

View File

@ -240,6 +240,7 @@ If an item contains spaces, it should be surrounded by ".
101038 | 101038 |
101039 | 101039 |
101040 | 101040 |
101047 |
Google-Apps | Google-Apps |
Google-Chrome-Device-Management | Google-Chrome-Device-Management |
Google-Drive-storage | Google-Drive-storage |
@ -264,6 +265,7 @@ If an item contains spaces, it should be surrounded by ".
cloudidentity | identity | 1010010001 | cloudidentity | identity | 1010010001 |
cloudidentitypremium | identitypremium | 1010050001 | cloudidentitypremium | identitypremium | 1010050001 |
cloudsearch | 1010350001 | cloudsearch | 1010350001 |
duetai | 101047001 |
gsuitebasic | gafb | gafw | basic | Google-Apps-For-Business | gsuitebasic | gafb | gafw | basic | Google-Apps-For-Business |
gsuitebusiness | gau | gsb | unlimited | Google-Apps-Unlimited | gsuitebusiness | gau | gsb | unlimited | Google-Apps-Unlimited |
gsuitebusinessarchived | gsbau | businessarchived | 1010340002 | gsuitebusinessarchived | gsbau | businessarchived | 1010340002 |

View File

@ -2,6 +2,16 @@
Merged GAM-Team version Merged GAM-Team version
6.63.17
Added support for Duet AI license.
* ProductID - 101047
* SKUID - 101047001 | duetai
Added `api_call_tries_limit` variable to `gam.cfg` that limits the number of tries
for Google API calls that return an error that indicates a retry should be performed.
The default value is 10 and the range of allowable values is 3-10.
6.63.16 6.63.16
Arguments `noinherit`, `blockinheritance` and `blockinheritance true` have been removed from the following Arguments `noinherit`, `blockinheritance` and `blockinheritance true` have been removed from the following

View File

@ -3054,15 +3054,15 @@ def getGSheetData():
f = TemporaryFile(mode='w+', encoding=UTF8) f = TemporaryFile(mode='w+', encoding=UTF8)
if GC.Values[GC.DEBUG_LEVEL] > 0: if GC.Values[GC.DEBUG_LEVEL] > 0:
sys.stderr.write(f'Debug: spreadsheetUrl: {spreadsheetUrl}\n') sys.stderr.write(f'Debug: spreadsheetUrl: {spreadsheetUrl}\n')
retries = 3 triesLimit = 3
for n in range(1, retries+1): for n in range(1, triesLimit+1):
_, content = drive._http.request(uri=spreadsheetUrl, method='GET') _, content = drive._http.request(uri=spreadsheetUrl, method='GET')
# Check for HTML error message instead of data # Check for HTML error message instead of data
if content[0:15] != b'<!DOCTYPE html>': if content[0:15] != b'<!DOCTYPE html>':
break break
tg = HTML_TITLE_PATTERN.match(content[0:600].decode('utf-8')) tg = HTML_TITLE_PATTERN.match(content[0:600].decode('utf-8'))
errMsg = tg.group(1) if tg else 'Unknown error' errMsg = tg.group(1) if tg else 'Unknown error'
getGDocSheetDataRetryWarning([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg, n, retries) getGDocSheetDataRetryWarning([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg, n, triesLimit)
time.sleep(20) time.sleep(20)
else: else:
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg) getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg)
@ -4553,16 +4553,16 @@ def getClientCredentials(forceRefresh=False, forceWrite=False, filename=None, ap
if not credentials: if not credentials:
invalidOauth2TxtExit('') invalidOauth2TxtExit('')
if credentials.expired or forceRefresh: if credentials.expired or forceRefresh:
retries = 3 triesLimit = 3
for n in range(1, retries+1): for n in range(1, triesLimit+1):
try: try:
credentials.refresh(transportCreateRequest()) credentials.refresh(transportCreateRequest())
if writeCreds or forceWrite: if writeCreds or forceWrite:
writeClientCredentials(credentials, filename or GC.Values[GC.OAUTH2_TXT]) writeClientCredentials(credentials, filename or GC.Values[GC.OAUTH2_TXT])
break break
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e: except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != retries: if n != triesLimit:
waitOnFailure(n, retries, NETWORK_ERROR_RC, str(e)) waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue continue
handleServerError(e) handleServerError(e)
except google.auth.exceptions.RefreshError as e: except google.auth.exceptions.RefreshError as e:
@ -4576,10 +4576,10 @@ def getClientCredentials(forceRefresh=False, forceWrite=False, filename=None, ap
handleOAuthTokenError(e, False) handleOAuthTokenError(e, False)
return credentials return credentials
def waitOnFailure(n, retries, error_code, error_message): def waitOnFailure(n, triesLimit, error_code, error_message):
delta = min(2 ** n, 60)+float(random.randint(1, 1000))/1000 delta = min(2 ** n, 60)+float(random.randint(1, 1000))/1000
if n > 3: if n > 3:
writeStderr(f'Temporary error: {error_code} - {error_message}, Backing off: {int(delta)} seconds, Retry: {n}/{retries}\n') writeStderr(f'Temporary error: {error_code} - {error_message}, Backing off: {int(delta)} seconds, Retry: {n}/{triesLimit}\n')
flushStderr() flushStderr()
time.sleep(delta) time.sleep(delta)
if GC.Values[GC.SHOW_API_CALLS_RETRY_DATA]: if GC.Values[GC.SHOW_API_CALLS_RETRY_DATA]:
@ -4614,8 +4614,8 @@ def getService(api, httpObj):
clearServiceCache(service) clearServiceCache(service)
return service return service
if not hasLocalJSON: if not hasLocalJSON:
retries = 3 triesLimit = 3
for n in range(1, retries+1): for n in range(1, triesLimit+1):
try: try:
service = googleapiclient.discovery.build(api, version, http=httpObj, cache_discovery=False, service = googleapiclient.discovery.build(api, version, http=httpObj, cache_discovery=False,
discoveryServiceUrl=DISCOVERY_URIS[v2discovery], static_discovery=False) discoveryServiceUrl=DISCOVERY_URIS[v2discovery], static_discovery=False)
@ -4627,20 +4627,20 @@ def getService(api, httpObj):
except googleapiclient.errors.UnknownApiNameOrVersion as e: except googleapiclient.errors.UnknownApiNameOrVersion as e:
systemErrorExit(GOOGLE_API_ERROR_RC, Msg.UNKNOWN_API_OR_VERSION.format(str(e), __author__)) systemErrorExit(GOOGLE_API_ERROR_RC, Msg.UNKNOWN_API_OR_VERSION.format(str(e), __author__))
except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e: except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e:
if n != retries: if n != triesLimit:
waitOnFailure(n, retries, INVALID_JSON_RC, str(e)) waitOnFailure(n, triesLimit, INVALID_JSON_RC, str(e))
continue continue
systemErrorExit(INVALID_JSON_RC, str(e)) systemErrorExit(INVALID_JSON_RC, str(e))
except (http_client.ResponseNotReady, OSError, googleapiclient.errors.HttpError) as e: except (http_client.ResponseNotReady, OSError, googleapiclient.errors.HttpError) as e:
errMsg = f'Connection error: {str(e) or repr(e)}' errMsg = f'Connection error: {str(e) or repr(e)}'
if n != retries: if n != triesLimit:
waitOnFailure(n, retries, SOCKET_ERROR_RC, errMsg) waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
continue continue
systemErrorExit(SOCKET_ERROR_RC, errMsg) systemErrorExit(SOCKET_ERROR_RC, errMsg)
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e: except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != retries: if n != triesLimit:
httpObj.connections = {} httpObj.connections = {}
waitOnFailure(n, retries, NETWORK_ERROR_RC, str(e)) waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue continue
handleServerError(e) handleServerError(e)
disc_file, discovery = readDiscoveryFile(f'{api}-{version}') disc_file, discovery = readDiscoveryFile(f'{api}-{version}')
@ -4897,27 +4897,28 @@ def checkGDataError(e, service):
def callGData(service, function, def callGData(service, function,
bailOnInternalServerError=False, softErrors=False, bailOnInternalServerError=False, softErrors=False,
throwErrors=None, retryErrors=None, throwErrors=None, retryErrors=None, triesLimit=0,
**kwargs): **kwargs):
if throwErrors is None: if throwErrors is None:
throwErrors = [] throwErrors = []
if retryErrors is None: if retryErrors is None:
retryErrors = [] retryErrors = []
if triesLimit == 0:
triesLimit = GC.Values[GC.API_CALLS_TRIES_LIMIT]
allRetryErrors = GDATA.NON_TERMINATING_ERRORS+retryErrors allRetryErrors = GDATA.NON_TERMINATING_ERRORS+retryErrors
method = getattr(service, function) method = getattr(service, function)
retries = 10
if GC.Values[GC.API_CALLS_RATE_CHECK]: if GC.Values[GC.API_CALLS_RATE_CHECK]:
checkAPICallsRate() checkAPICallsRate()
for n in range(1, retries+1): for n in range(1, triesLimit+1):
try: try:
return method(**kwargs) return method(**kwargs)
except (gdata.service.RequestError, gdata.apps.service.AppsForYourDomainException) as e: except (gdata.service.RequestError, gdata.apps.service.AppsForYourDomainException) as e:
error_code, error_message = checkGDataError(e, service) error_code, error_message = checkGDataError(e, service)
if (n != retries) and (error_code in allRetryErrors): if (n != triesLimit) and (error_code in allRetryErrors):
if (error_code == GDATA.INTERNAL_SERVER_ERROR and if (error_code == GDATA.INTERNAL_SERVER_ERROR and
bailOnInternalServerError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]): bailOnInternalServerError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]):
raise GDATA.ERROR_CODE_EXCEPTION_MAP[error_code](error_message) raise GDATA.ERROR_CODE_EXCEPTION_MAP[error_code](error_message)
waitOnFailure(n, retries, error_code, error_message) waitOnFailure(n, triesLimit, error_code, error_message)
continue continue
if error_code in throwErrors: if error_code in throwErrors:
if error_code in GDATA.ERROR_CODE_EXCEPTION_MAP: if error_code in GDATA.ERROR_CODE_EXCEPTION_MAP:
@ -4930,8 +4931,8 @@ def callGData(service, function,
APIAccessDeniedExit() APIAccessDeniedExit()
systemErrorExit(GOOGLE_API_ERROR_RC, f'{error_code} - {error_message}') systemErrorExit(GOOGLE_API_ERROR_RC, f'{error_code} - {error_message}')
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e: except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != retries: if n != triesLimit:
waitOnFailure(n, retries, NETWORK_ERROR_RC, str(e)) waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue continue
handleServerError(e) handleServerError(e)
except google.auth.exceptions.RefreshError as e: except google.auth.exceptions.RefreshError as e:
@ -4941,8 +4942,8 @@ def callGData(service, function,
raise GDATA.ERROR_CODE_EXCEPTION_MAP[GDATA.SERVICE_NOT_APPLICABLE](str(e)) raise GDATA.ERROR_CODE_EXCEPTION_MAP[GDATA.SERVICE_NOT_APPLICABLE](str(e))
except (http_client.ResponseNotReady, OSError) as e: except (http_client.ResponseNotReady, OSError) as e:
errMsg = f'Connection error: {str(e) or repr(e)}' errMsg = f'Connection error: {str(e) or repr(e)}'
if n != retries: if n != triesLimit:
waitOnFailure(n, retries, SOCKET_ERROR_RC, errMsg) waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
continue continue
if softErrors: if softErrors:
writeStderr(f'\n{ERROR_PREFIX}{errMsg} - Giving up.\n') writeStderr(f'\n{ERROR_PREFIX}{errMsg} - Giving up.\n')
@ -5163,18 +5164,20 @@ def checkGAPIError(e, softErrors=False, retryOnHttpError=False, mapNotFound=True
def callGAPI(service, function, def callGAPI(service, function,
bailOnInternalError=False, bailOnTransientError=False, bailOnInvalidError=False, bailOnInternalError=False, bailOnTransientError=False, bailOnInvalidError=False,
softErrors=False, mapNotFound=True, softErrors=False, mapNotFound=True,
throwReasons=None, retryReasons=None, retries=10, throwReasons=None, retryReasons=None, triesLimit=0,
**kwargs): **kwargs):
if throwReasons is None: if throwReasons is None:
throwReasons = [] throwReasons = []
if retryReasons is None: if retryReasons is None:
retryReasons = [] retryReasons = []
if triesLimit == 0:
triesLimit = GC.Values[GC.API_CALLS_TRIES_LIMIT]
allRetryReasons = GAPI.DEFAULT_RETRY_REASONS+retryReasons allRetryReasons = GAPI.DEFAULT_RETRY_REASONS+retryReasons
method = getattr(service, function) method = getattr(service, function)
svcparms = dict(list(kwargs.items())+GM.Globals[GM.EXTRA_ARGS_LIST]) svcparms = dict(list(kwargs.items())+GM.Globals[GM.EXTRA_ARGS_LIST])
if GC.Values[GC.API_CALLS_RATE_CHECK]: if GC.Values[GC.API_CALLS_RATE_CHECK]:
checkAPICallsRate() checkAPICallsRate()
for n in range(1, retries+1): for n in range(1, triesLimit+1):
try: try:
return method(**svcparms).execute() return method(**svcparms).execute()
except googleapiclient.errors.HttpError as e: except googleapiclient.errors.HttpError as e:
@ -5190,7 +5193,7 @@ def callGAPI(service, function,
continue continue
if http_status == 0: if http_status == 0:
return None return None
if (n != retries) and ((reason in allRetryReasons) or if (n != triesLimit) and ((reason in allRetryReasons) or
(GC.Values[GC.RETRY_API_SERVICE_NOT_AVAILABLE] and (reason == GAPI.SERVICE_NOT_AVAILABLE))): (GC.Values[GC.RETRY_API_SERVICE_NOT_AVAILABLE] and (reason == GAPI.SERVICE_NOT_AVAILABLE))):
if (reason in [GAPI.INTERNAL_ERROR, GAPI.BACKEND_ERROR] and if (reason in [GAPI.INTERNAL_ERROR, GAPI.BACKEND_ERROR] and
bailOnInternalError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]): bailOnInternalError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]):
@ -5198,7 +5201,7 @@ def callGAPI(service, function,
if (reason in [GAPI.INVALID] and if (reason in [GAPI.INVALID] and
bailOnInvalidError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]): bailOnInvalidError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]):
raise GAPI.REASON_EXCEPTION_MAP[reason](message) raise GAPI.REASON_EXCEPTION_MAP[reason](message)
waitOnFailure(n, retries, reason, message) waitOnFailure(n, triesLimit, reason, message)
if reason == GAPI.TRANSIENT_ERROR and bailOnTransientError: if reason == GAPI.TRANSIENT_ERROR and bailOnTransientError:
raise GAPI.REASON_EXCEPTION_MAP[reason](message) raise GAPI.REASON_EXCEPTION_MAP[reason](message)
continue continue
@ -5213,9 +5216,9 @@ def callGAPI(service, function,
APIAccessDeniedExit() APIAccessDeniedExit()
systemErrorExit(HTTP_ERROR_RC, formatHTTPError(http_status, reason, message)) systemErrorExit(HTTP_ERROR_RC, formatHTTPError(http_status, reason, message))
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e: except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != retries: if n != triesLimit:
service._http.connections = {} service._http.connections = {}
waitOnFailure(n, retries, NETWORK_ERROR_RC, str(e)) waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue continue
handleServerError(e) handleServerError(e)
except google.auth.exceptions.RefreshError as e: except google.auth.exceptions.RefreshError as e:
@ -5225,8 +5228,8 @@ def callGAPI(service, function,
raise GAPI.REASON_EXCEPTION_MAP[GAPI.SERVICE_NOT_AVAILABLE](str(e)) raise GAPI.REASON_EXCEPTION_MAP[GAPI.SERVICE_NOT_AVAILABLE](str(e))
except (http_client.ResponseNotReady, OSError) as e: except (http_client.ResponseNotReady, OSError) as e:
errMsg = f'Connection error: {str(e) or repr(e)}' errMsg = f'Connection error: {str(e) or repr(e)}'
if n != retries: if n != triesLimit:
waitOnFailure(n, retries, SOCKET_ERROR_RC, errMsg) waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
continue continue
if softErrors: if softErrors:
writeStderr(f'\n{ERROR_PREFIX}{errMsg} - Giving up.\n') writeStderr(f'\n{ERROR_PREFIX}{errMsg} - Giving up.\n')
@ -5443,22 +5446,22 @@ def buildGAPIServiceObject(api, user, i=0, count=0, displayError=True):
service = getService(api, httpObj) service = getService(api, httpObj)
credentials = getSvcAcctCredentials(api, userEmail) credentials = getSvcAcctCredentials(api, userEmail)
request = transportCreateRequest(httpObj) request = transportCreateRequest(httpObj)
retries = 3 triesLimit = 3
for n in range(1, retries+1): for n in range(1, triesLimit+1):
try: try:
credentials.refresh(request) credentials.refresh(request)
service._http = transportAuthorizedHttp(credentials, http=httpObj) service._http = transportAuthorizedHttp(credentials, http=httpObj)
return (userEmail, service) return (userEmail, service)
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e: except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != retries: if n != triesLimit:
httpObj.connections = {} httpObj.connections = {}
waitOnFailure(n, retries, NETWORK_ERROR_RC, str(e)) waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue continue
handleServerError(e) handleServerError(e)
except google.auth.exceptions.RefreshError as e: except google.auth.exceptions.RefreshError as e:
if isinstance(e.args, tuple): if isinstance(e.args, tuple):
e = e.args[0] e = e.args[0]
if n < retries: if n < triesLimit:
if isinstance(e, str): if isinstance(e, str):
eContent = e eContent = e
else: else:
@ -8875,16 +8878,16 @@ def _getServerTLSUsed(location):
_, netloc, _, _, _, _ = urlparse(url) _, netloc, _, _, _, _ = urlparse(url)
conn = 'https:'+netloc conn = 'https:'+netloc
httpObj = getHttpObj() httpObj = getHttpObj()
retries = 5 triesLimit = 5
for n in range(1, retries+1): for n in range(1, triesLimit+1):
try: try:
httpObj.request(url, headers={'user-agent': GAM_USER_AGENT}) httpObj.request(url, headers={'user-agent': GAM_USER_AGENT})
cipher_name, tls_ver, _ = httpObj.connections[conn].sock.cipher() cipher_name, tls_ver, _ = httpObj.connections[conn].sock.cipher()
return tls_ver, cipher_name return tls_ver, cipher_name
except (httplib2.HttpLib2Error, RuntimeError) as e: except (httplib2.HttpLib2Error, RuntimeError) as e:
if n != retries: if n != triesLimit:
httpObj.connections = {} httpObj.connections = {}
waitOnFailure(n, retries, NETWORK_ERROR_RC, str(e)) waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue continue
handleServerError(e) handleServerError(e)
@ -24552,8 +24555,12 @@ def getChatSpace(myarg):
chatSpace = getString(Cmd.OB_CHAT_SPACE) chatSpace = getString(Cmd.OB_CHAT_SPACE)
if chatSpace.startswith('spaces/'): if chatSpace.startswith('spaces/'):
return chatSpace return chatSpace
if not chatSpace.startswith('space/'):
return 'spaces/'+chatSpace
_, chatSpace = chatSpace.split('/', 1)
else: # myarg.startswith('spaces/') or myarg.startswith('space/')
_, chatSpace = Cmd.Previous().split('/', 1)
return 'spaces/'+chatSpace return 'spaces/'+chatSpace
return Cmd.Previous() # /spaces/xxx
def _cleanChatSpace(space): def _cleanChatSpace(space):
space.pop('type', None) space.pop('type', None)
@ -24709,7 +24716,7 @@ def updateChatSpace(users):
body = {} body = {}
while Cmd.ArgumentsRemaining(): while Cmd.ArgumentsRemaining():
myarg = getArgument() myarg = getArgument()
if myarg == 'space' or myarg.startswith('spaces/'): if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
name = getChatSpace(myarg) name = getChatSpace(myarg)
elif getChatSpaceParameters(myarg, body, CHAT_UPDATE_SPACE_TYPE_MAP): elif getChatSpaceParameters(myarg, body, CHAT_UPDATE_SPACE_TYPE_MAP):
pass pass
@ -24741,7 +24748,7 @@ def deleteChatSpace(users):
name = None name = None
while Cmd.ArgumentsRemaining(): while Cmd.ArgumentsRemaining():
myarg = getArgument() myarg = getArgument()
if myarg == 'space' or myarg.startswith('spaces/'): if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
name = getChatSpace(myarg) name = getChatSpace(myarg)
else: else:
unknownArgumentExit() unknownArgumentExit()
@ -24768,7 +24775,7 @@ def infoChatSpace(users, name=None):
function = 'get' if name is None else 'findDirectMessage' function = 'get' if name is None else 'findDirectMessage'
while Cmd.ArgumentsRemaining(): while Cmd.ArgumentsRemaining():
myarg = getArgument() myarg = getArgument()
if function == 'get' and (myarg == 'space' or myarg.startswith('spaces/')): if function == 'get' and (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
name = getChatSpace(myarg) name = getChatSpace(myarg)
else: else:
FJQC.GetFormatJSON(myarg) FJQC.GetFormatJSON(myarg)
@ -24917,7 +24924,7 @@ CHAT_MEMBER_TYPE_MAP = {
# gam <UserTypeEntity> create chatmember <ChatSpace> # gam <UserTypeEntity> create chatmember <ChatSpace>
# [type human|bot] # [type human|bot]
# (user <UserItem>)* (members <UserTypeEntity>)* # (user <UserItem>)* (members <UserTypeEntity>)* (group <GroupItem>)*
# [formatjson|returnidonly] # [formatjson|returnidonly]
def createChatMember(users): def createChatMember(users):
def addMembers(members, field, entityType, i, count): def addMembers(members, field, entityType, i, count):
@ -24953,16 +24960,19 @@ def createChatMember(users):
parent = None parent = None
mtype = CHAT_MEMBER_TYPE_MAP['human'] mtype = CHAT_MEMBER_TYPE_MAP['human']
userList = [] userList = []
groupList = []
returnIdOnly = False returnIdOnly = False
while Cmd.ArgumentsRemaining(): while Cmd.ArgumentsRemaining():
myarg = getArgument() myarg = getArgument()
if myarg == 'space' or myarg.startswith('spaces/'): if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parent = getChatSpace(myarg) parent = getChatSpace(myarg)
elif myarg == 'user': elif myarg == 'user':
userList.append(getEmailAddress(returnUIDprefix='uid:')) userList.append(getEmailAddress(returnUIDprefix='uid:'))
elif myarg in {'member', 'members'}: elif myarg in {'member', 'members'}:
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS) _, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
userList.extend(members) userList.extend(members)
elif myarg == 'group':
groupList.append(getEmailAddress(returnUIDprefix='uid:'))
elif myarg == 'type': elif myarg == 'type':
mtype = getChoice(CHAT_MEMBER_TYPE_MAP, mapChoice=True) mtype = getChoice(CHAT_MEMBER_TYPE_MAP, mapChoice=True)
elif myarg == 'returnidonly': elif myarg == 'returnidonly':
@ -24971,54 +24981,79 @@ def createChatMember(users):
FJQC.GetFormatJSON(myarg) FJQC.GetFormatJSON(myarg)
if not parent: if not parent:
missingArgumentExit('space') missingArgumentExit('space')
if not userList: if not userList and not groupList:
missingArgumentExit('user|members') missingArgumentExit('user|members|group')
userMembers = [] userMembers = []
for user in userList: for user in userList:
name = normalizeEmailAddressOrUID(user) name = normalizeEmailAddressOrUID(user)
userMembers.append({'member': {'name': f'users/{name}', 'type': mtype}}) userMembers.append({'member': {'name': f'users/{name}', 'type': mtype}})
groupMembers = []
for group in groupList:
name = normalizeEmailAddressOrUID(group)
groupMembers.append({'groupMember': {'name': f'groups/{name}'}})
i, count, users = getEntityArgument(users) i, count, users = getEntityArgument(users)
for user in users: for user in users:
i += 1 i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MEMBERSHIPS, user, i, count, [Ent.CHAT_SPACE, parent, Ent.CHAT_MEMBER, '']) user, chat, kvList = buildChatServiceObject(API.CHAT_MEMBERSHIPS, user, i, count, [Ent.CHAT_SPACE, parent, Ent.CHAT_MEMBER, ''])
if not chat: if not chat:
continue continue
if userMembers:
addMembers(userMembers, 'member', Ent.USER, i, count) addMembers(userMembers, 'member', Ent.USER, i, count)
if groupMembers:
addMembers(groupMembers, 'groupMember', Ent.GROUP, i, count)
CHAT_MEMBER_ROLE_CHOICES_MAP = {
'member': 'ROLE_MEMBER',
'manager': 'ROLE_MANAGER'
}
# gam <UserTypeEntity> delete chatmember <ChatSpace> # gam <UserTypeEntity> delete chatmember <ChatSpace>
# ((user <UserItem>)|(members <UserTypeEntity>))+ # ((user <UserItem>)|(members <UserTypeEntity>)|(group <GroupItem>))+
# gam <UserTypeEntity> remove chatmember members <ChatMemberList> # gam <UserTypeEntity> remove chatmember members <ChatMemberList>
def deleteChatMember(users): # gam <UserTypeEntity> update chatmember <ChatSpace>
# ((user <UserItem>)|(members <UserTypeEntity>))+ role member|manager
# gam <UserTypeEntity> modify chatmember members <ChatMemberList> role member|manager
def deleteUpdateChatMember(users):
cd = buildGAPIObject(API.DIRECTORY)
action = Act.Get() action = Act.Get()
deleteMode = action in {Act.DELETE, Act.REMOVE}
parent = None parent = None
body = {}
memberNames = [] memberNames = []
userList = [] userGroupList = []
while Cmd.ArgumentsRemaining(): while Cmd.ArgumentsRemaining():
myarg = getArgument() myarg = getArgument()
if action == Act.REMOVE: if action in {Act.UPDATE, Act.MODIFY} and myarg == 'role':
body['role'] = getChoice(CHAT_MEMBER_ROLE_CHOICES_MAP, mapChoice=True)
continue
if action in {Act.REMOVE, Act.MODIFY}:
if myarg in {'member', 'members'}: if myarg in {'member', 'members'}:
memberNames.extend(getString(Cmd.OB_CHAT_MEMBER).replace(',', ' ').split()) memberNames.extend(getString(Cmd.OB_CHAT_MEMBER).replace(',', ' ').split())
else: else:
unknownArgumentExit() unknownArgumentExit()
else: # Act.DELETE else: # {Act.DELETE, Act.UPDATE}
if myarg == 'space' or myarg.startswith('spaces/'): if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parent = getChatSpace(myarg) parent = getChatSpace(myarg)
elif myarg == 'user': elif myarg == 'user':
userList.append(getEmailAddress(returnUIDprefix='uid:')) userGroupList.append(getEmailAddress(returnUIDprefix='uid:'))
elif myarg in {'member', 'members'}: elif myarg in {'member', 'members'}:
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS) _, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
userList.extend(members) userGroupList.extend(members)
elif deleteMode and myarg == 'group':
userGroupList.append(getEmailAddress(returnUIDprefix='uid:'))
else: else:
unknownArgumentExit() unknownArgumentExit()
if action == Act.REMOVE: if not deleteMode and 'role' not in body:
missingArgumentExit('role')
if action in {Act.REMOVE, Act.MODIFY}:
if not memberNames: if not memberNames:
missingArgumentExit('members') missingArgumentExit('members')
else: # Act.DELETE else: # {Act.DELETE, Act.UPDATE}
if not parent: if not parent:
missingArgumentExit('space') missingArgumentExit('space')
if not userList: if not userGroupList:
missingArgumentExit('user|members') missingArgumentExit('user|members|group')
for user in userList: for user in userGroupList:
name = normalizeEmailAddressOrUID(user) name = normalizeEmailAddressOrUID(user)
memberNames.append(f'{parent}/members/{name}') memberNames.append(f'{parent}/members/{name}')
i, count, users = getEntityArgument(users) i, count, users = getEntityArgument(users)
@ -25036,11 +25071,21 @@ def deleteChatMember(users):
j += 1 j += 1
kvList[-1] = name kvList[-1] = name
try: try:
if deleteMode:
callGAPI(chat.spaces().members(), 'delete', callGAPI(chat.spaces().members(), 'delete',
bailOnInternalError=True, bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=name) name=name)
entityActionPerformed(kvList, j, jcount) entityActionPerformed(kvList, j, jcount)
else:
member = callGAPI(chat.spaces().members(), 'patch',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=name, updateMask='role', body=body)
_getChatMemberEmail(cd, member)
Ind.Increment()
_showChatMember(member, None, j, jcount)
Ind.Decrement()
except GAPI.notFound as e: except GAPI.notFound as e:
entityActionFailedWarning(kvList, str(e), j, jcount) entityActionFailedWarning(kvList, str(e), j, jcount)
except (GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e: except (GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
@ -25119,7 +25164,7 @@ def printShowChatMembers(users):
myarg = getArgument() myarg = getArgument()
if csvPF and myarg == 'todrive': if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters() csvPF.GetTodriveParameters()
elif myarg == 'space' or myarg.startswith('spaces/'): elif myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parent = getChatSpace(myarg) parent = getChatSpace(myarg)
elif myarg == 'showinvited': elif myarg == 'showinvited':
kwargs['showInvited'] = getBoolean() kwargs['showInvited'] = getBoolean()
@ -25197,7 +25242,7 @@ def createChatMessage(users):
returnIdOnly = False returnIdOnly = False
while Cmd.ArgumentsRemaining(): while Cmd.ArgumentsRemaining():
myarg = getArgument() myarg = getArgument()
if myarg == 'space' or myarg.startswith('spaces/'): if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parent = getChatSpace(myarg) parent = getChatSpace(myarg)
elif myarg == 'thread': elif myarg == 'thread':
body.setdefault('thread', {}) body.setdefault('thread', {})
@ -25399,7 +25444,7 @@ def printShowChatMessages(users):
myarg = getArgument() myarg = getArgument()
if csvPF and myarg == 'todrive': if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters() csvPF.GetTodriveParameters()
elif myarg == 'space' or myarg.startswith('spaces/'): elif myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parent = getChatSpace(myarg) parent = getChatSpace(myarg)
elif myarg == 'showdeleted': elif myarg == 'showdeleted':
showDeleted = getBoolean() showDeleted = getBoolean()
@ -45224,7 +45269,7 @@ def _batchAddItemsToCourse(croom, courseId, i, count, addParticipants, role):
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR, throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR,
GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION, GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION,
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE], GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], retries=10 if reason != GAPI.NOT_FOUND else 3, retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], triesLimit=0 if reason != GAPI.NOT_FOUND else 3,
courseId=addCourseIdScope(ri[RI_ENTITY]), courseId=addCourseIdScope(ri[RI_ENTITY]),
body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])}, body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])},
fields='') fields='')
@ -45301,7 +45346,7 @@ def _batchRemoveItemsFromCourse(croom, courseId, i, count, removeParticipants, r
callGAPI(service, 'delete', callGAPI(service, 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED,
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE], GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], retries=10 if reason != GAPI.NOT_FOUND else 3, retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], triesLimit=0 if reason != GAPI.NOT_FOUND else 3,
courseId=addCourseIdScope(ri[RI_ENTITY]), courseId=addCourseIdScope(ri[RI_ENTITY]),
body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])}, body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])},
fields='') fields='')
@ -57044,14 +57089,14 @@ def transferDrive(users):
if removeSourceParents: if removeSourceParents:
op = 'Remove Source Parents' op = 'Remove Source Parents'
callGAPI(sourceDrive.files(), 'update', callGAPI(sourceDrive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS, retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], retries=3, throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS, retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3,
fileId=childFileId, removeParents=','.join(removeSourceParents), fields='') fileId=childFileId, removeParents=','.join(removeSourceParents), fields='')
actionUser = targetUser actionUser = targetUser
if addTargetParent or removeTargetParents: if addTargetParent or removeTargetParents:
op = 'Add/Remove Target Parents' op = 'Add/Remove Target Parents'
callGAPI(targetDrive.files(), 'update', callGAPI(targetDrive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INSUFFICIENT_PARENT_PERMISSIONS], throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], retries=3, retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3,
fileId=childFileId, fileId=childFileId,
addParents=addTargetParent, removeParents=','.join(removeTargetParents), fields='') addParents=addTargetParent, removeParents=','.join(removeTargetParents), fields='')
entityModifierNewValueItemValueListActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount) entityModifierNewValueItemValueListActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount)
@ -57168,7 +57213,7 @@ def transferDrive(users):
try: try:
callGAPI(sourceDrive.files(), 'update', callGAPI(sourceDrive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST], throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST],
retryReasons=[GAPI.FILE_NOT_FOUND], retries=3, retryReasons=[GAPI.FILE_NOT_FOUND], triesLimit=3,
fileId=childFileId, fileId=childFileId,
removeParents=','.join(existingParentIds), body={}, fields='') removeParents=','.join(existingParentIds), body={}, fields='')
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError, except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
@ -57184,7 +57229,7 @@ def transferDrive(users):
# try: # try:
# callGAPI(targetDrive.files(), 'update', # callGAPI(targetDrive.files(), 'update',
# throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS], # throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
# retryReasons=[GAPI.FILE_NOT_FOUND], retries=3, # retryReasons=[GAPI.FILE_NOT_FOUND], triesLimit=3,
# fileId=childFileId, # fileId=childFileId,
# addParents=mappedParentId, body={}, fields='') # addParents=mappedParentId, body={}, fields='')
# except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.unknownError, # except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.unknownError,
@ -59370,6 +59415,8 @@ def printShowDriveFileACLs(users, useDomainAdminAccess=False):
useDomainAdminAccess = True useDomainAdminAccess = True
elif myarg == 'pmselect': elif myarg == 'pmselect':
pmselect = True pmselect = True
elif myarg == 'pmfilter': # Ignore, this is the default behavior
pass
elif PM.ProcessArgument(myarg): elif PM.ProcessArgument(myarg):
pass pass
elif myarg == 'includepermissionsforview': elif myarg == 'includepermissionsforview':
@ -66987,13 +67034,14 @@ def _processForwardingAddress(user, i, count, emailAddress, j, jcount, gmail, fu
userDefined = True userDefined = True
try: try:
result = callGAPI(gmail.users().settings().forwardingAddresses(), function, result = callGAPI(gmail.users().settings().forwardingAddresses(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS, GAPI.DUPLICATE, GAPI.INVALID_ARGUMENT], throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS, GAPI.DUPLICATE,
GAPI.INVALID_ARGUMENT, GAPI.FAILED_PRECONDITION],
userId='me', **kwargs) userId='me', **kwargs)
if function == 'get': if function == 'get':
_showForwardingAddress(j, count, result) _showForwardingAddress(j, count, result)
else: else:
entityActionPerformed([Ent.USER, user, Ent.FORWARDING_ADDRESS, emailAddress], j, jcount) entityActionPerformed([Ent.USER, user, Ent.FORWARDING_ADDRESS, emailAddress], j, jcount)
except (GAPI.notFound, GAPI.alreadyExists, GAPI.duplicate, GAPI.invalidArgument) as e: except (GAPI.notFound, GAPI.alreadyExists, GAPI.duplicate, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.FORWARDING_ADDRESS, emailAddress], str(e), j, jcount) entityActionFailedWarning([Ent.USER, user, Ent.FORWARDING_ADDRESS, emailAddress], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.badRequest): except (GAPI.serviceNotAvailable, GAPI.badRequest):
entityServiceNotApplicableWarning(Ent.USER, user, i, count) entityServiceNotApplicableWarning(Ent.USER, user, i, count)
@ -70719,7 +70767,7 @@ USER_COMMANDS_WITH_OBJECTS = {
Cmd.ARG_BACKUPCODE: deleteBackupCodes, Cmd.ARG_BACKUPCODE: deleteBackupCodes,
Cmd.ARG_CALENDAR: deleteCalendars, Cmd.ARG_CALENDAR: deleteCalendars,
Cmd.ARG_CALENDARACL: deleteCalendarACLs, Cmd.ARG_CALENDARACL: deleteCalendarACLs,
Cmd.ARG_CHATMEMBER: deleteChatMember, Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
Cmd.ARG_CHATMESSAGE: deleteChatMessage, Cmd.ARG_CHATMESSAGE: deleteChatMessage,
Cmd.ARG_CHATSPACE: deleteChatSpace, Cmd.ARG_CHATSPACE: deleteChatSpace,
Cmd.ARG_CLASSROOMINVITATION: deleteClassroomInvitations, Cmd.ARG_CLASSROOMINVITATION: deleteClassroomInvitations,
@ -70837,6 +70885,7 @@ USER_COMMANDS_WITH_OBJECTS = {
'modify': 'modify':
(Act.MODIFY, (Act.MODIFY,
{Cmd.ARG_CALENDAR: modifyCalendars, {Cmd.ARG_CALENDAR: modifyCalendars,
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
Cmd.ARG_MESSAGE: processMessages, Cmd.ARG_MESSAGE: processMessages,
Cmd.ARG_THREAD: processThreads, Cmd.ARG_THREAD: processThreads,
} }
@ -70939,7 +70988,7 @@ USER_COMMANDS_WITH_OBJECTS = {
'remove': 'remove':
(Act.REMOVE, (Act.REMOVE,
{Cmd.ARG_CALENDAR: removeCalendars, {Cmd.ARG_CALENDAR: removeCalendars,
Cmd.ARG_CHATMEMBER: deleteChatMember, Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
} }
), ),
'replacedomain': 'replacedomain':
@ -71083,6 +71132,7 @@ USER_COMMANDS_WITH_OBJECTS = {
Cmd.ARG_CALATTENDEES: updateCalendarAttendees, Cmd.ARG_CALATTENDEES: updateCalendarAttendees,
Cmd.ARG_CALENDAR: updateCalendars, Cmd.ARG_CALENDAR: updateCalendars,
Cmd.ARG_CALENDARACL: updateCalendarACLs, Cmd.ARG_CALENDARACL: updateCalendarACLs,
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
Cmd.ARG_CHATMESSAGE: updateChatMessage, Cmd.ARG_CHATMESSAGE: updateChatMessage,
Cmd.ARG_CHATSPACE: updateChatSpace, Cmd.ARG_CHATSPACE: updateChatSpace,
Cmd.ARG_LOOKERSTUDIOPERMISSION: processLookerStudioPermissions, Cmd.ARG_LOOKERSTUDIOPERMISSION: processLookerStudioPermissions,

View File

@ -50,6 +50,8 @@ ADMIN_EMAIL = 'admin_email'
API_CALLS_RATE_CHECK = 'api_calls_rate_check' API_CALLS_RATE_CHECK = 'api_calls_rate_check'
# API calls per 100 seconds limit # API calls per 100 seconds limit
API_CALLS_RATE_LIMIT = 'api_calls_rate_limit' API_CALLS_RATE_LIMIT = 'api_calls_rate_limit'
# API calls tries limit
API_CALLS_TRIES_LIMIT = 'api_calls_tries_limit'
# Automatically generate gam batch command if number of users specified in gam users xxx command exceeds this number # Automatically generate gam batch command if number of users specified in gam users xxx command exceeds this number
# Default: 0, do not automatically generate gam batch commands # Default: 0, do not automatically generate gam batch commands
AUTO_BATCH_MIN = 'auto_batch_min' AUTO_BATCH_MIN = 'auto_batch_min'
@ -297,6 +299,7 @@ Defaults = {
ADMIN_EMAIL: '', ADMIN_EMAIL: '',
API_CALLS_RATE_CHECK: FALSE, API_CALLS_RATE_CHECK: FALSE,
API_CALLS_RATE_LIMIT: '100', API_CALLS_RATE_LIMIT: '100',
API_CALLS_TRIES_LIMIT: '10',
AUTO_BATCH_MIN: '0', AUTO_BATCH_MIN: '0',
BAIL_ON_INTERNAL_ERROR_TRIES: '2', BAIL_ON_INTERNAL_ERROR_TRIES: '2',
BATCH_SIZE: '50', BATCH_SIZE: '50',
@ -448,6 +451,7 @@ VAR_INFO = {
ADMIN_EMAIL: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'GA_ADMIN_EMAIL', VAR_LIMITS: (0, None)}, ADMIN_EMAIL: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'GA_ADMIN_EMAIL', VAR_LIMITS: (0, None)},
API_CALLS_RATE_CHECK: {VAR_TYPE: TYPE_BOOLEAN}, API_CALLS_RATE_CHECK: {VAR_TYPE: TYPE_BOOLEAN},
API_CALLS_RATE_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (50, None)}, API_CALLS_RATE_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (50, None)},
API_CALLS_TRIES_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (3, 10)},
AUTO_BATCH_MIN: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_AUTOBATCH', VAR_LIMITS: (0, 100)}, AUTO_BATCH_MIN: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_AUTOBATCH', VAR_LIMITS: (0, 100)},
BAIL_ON_INTERNAL_ERROR_TRIES: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10)}, BAIL_ON_INTERNAL_ERROR_TRIES: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10)},
BATCH_SIZE: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_BATCH_SIZE', VAR_LIMITS: (1, 1000)}, BATCH_SIZE: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_BATCH_SIZE', VAR_LIMITS: (1, 1000)},

View File

@ -33,6 +33,7 @@ _PRODUCTS = {
'101038': 'AppSheet', '101038': 'AppSheet',
'101039': 'Assured Controls', '101039': 'Assured Controls',
'101040': 'Beyond Corp Enterprise', '101040': 'Beyond Corp Enterprise',
'101047': 'Duet AI',
'Google-Apps': 'Google Workspace', 'Google-Apps': 'Google Workspace',
'Google-Chrome-Device-Management': 'Google Chrome Device Management', 'Google-Chrome-Device-Management': 'Google Chrome Device Management',
'Google-Drive-storage': 'Google Drive Storage', 'Google-Drive-storage': 'Google Drive Storage',
@ -81,6 +82,8 @@ _SKUS = {
'product': '101039', 'aliases': ['assuredcontrols'], 'displayName': 'Assured Controls'}, 'product': '101039', 'aliases': ['assuredcontrols'], 'displayName': 'Assured Controls'},
'1010400001': { '1010400001': {
'product': '101040', 'aliases': ['beyondcorp', 'beyondcorpenterprise', 'bce'], 'displayName': 'Beyond Corp Enterprise'}, 'product': '101040', 'aliases': ['beyondcorp', 'beyondcorpenterprise', 'bce'], 'displayName': 'Beyond Corp Enterprise'},
'1010470001': {
'product': '101047', 'aliases': ['duetai'], 'displayName': 'Duet AI for Enterprise'},
'Google-Apps': { 'Google-Apps': {
'product': 'Google-Apps', 'aliases': ['standard', 'free'], 'displayName': 'G Suite Legacy'}, 'product': 'Google-Apps', 'aliases': ['standard', 'free'], 'displayName': 'G Suite Legacy'},
'Google-Apps-For-Business': { 'Google-Apps-For-Business': {