From df04318366de95c50e9e57780b900e7aaa814a1b Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Tue, 28 Jan 2020 07:06:42 -0600 Subject: [PATCH] Deprecate Python 3.5, start to use fstrings --- .travis.yml | 10 -- src/controlflow.py | 7 +- src/gam.py | 254 +++++++++++++++++++++++---------------------- src/var.py | 8 +- 4 files changed, 134 insertions(+), 145 deletions(-) diff --git a/.travis.yml b/.travis.yml index f1f51477..84d9cec3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -96,16 +96,6 @@ matrix: - GAMOS=linux - PLATFORM=x86_64 - VMTYPE=build - - os: linux - name: "Python 3.5 Source Testing" - dist: xenial - language: python - python: - - "3.5" - env: - - GAMOS=linux - - PLATFORM=x86_64 - - VMTYPE=test - os: linux name: "Python 3.6 Source Testing" dist: bionic diff --git a/src/controlflow.py b/src/controlflow.py index 20a3d1a2..fef41e7c 100644 --- a/src/controlflow.py +++ b/src/controlflow.py @@ -58,9 +58,8 @@ def wait_on_failure(current_attempt_num, wait_on_fail = min(2**current_attempt_num, 60) + float(random.randint(1, 1000)) / 1000 if current_attempt_num > error_print_threshold: - sys.stderr.write( - 'Temporary error: {0}, Backing off: {1} seconds, Retry: {2}/{3}\n' - .format(error_message, int(wait_on_fail), current_attempt_num, - total_num_retries)) + sys.stderr.write((f'Temporary error: {error_message}, Backing off: ' + f'{int(wait_on_fail)} seconds, Retry: ' + f'{current_attempt_num}/{total_num_retries}\n')) sys.stderr.flush() time.sleep(wait_on_fail) diff --git a/src/gam.py b/src/gam.py index 767c99ec..fa1b735c 100755 --- a/src/gam.py +++ b/src/gam.py @@ -35,10 +35,7 @@ import mimetypes import os import platform import random -try: - from secrets import SystemRandom -except ImportError: - from random import SystemRandom +from secrets import SystemRandom import re import shlex import signal @@ -117,16 +114,16 @@ gam.exe update group announcements add member jsmith ''') def currentCount(i, count): - return ' ({0}/{1})'.format(i, count) if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else '' + return f' ({i}/{count})' if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else '' def currentCountNL(i, count): - return ' ({0}/{1})\n'.format(i, count) if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else '\n' + return f' ({i}/{count})\n' if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else '\n' def printGettingAllItems(items, query): if query: - sys.stderr.write("Getting all {0} in G Suite account that match query ({1}) (may take some time on a large account)...\n".format(items, query)) + sys.stderr.write(f"Getting all {items} in G Suite account that match query ({query}) (may take some time on a large account)...\n") else: - sys.stderr.write("Getting all {0} in G Suite account (may take some time on a large account)...\n".format(items)) + sys.stderr.write(f"Getting all {items} in G Suite account (may take some time on a large account)...\n") def entityServiceNotApplicableWarning(entityType, entityName, i, count): sys.stderr.write('{0}: {1}, Service not applicable/Does not exist{2}'.format(entityType, entityName, currentCountNL(i, count))) @@ -679,7 +676,7 @@ def doGAMCheckForUpdates(forceCheck=False): fileutils.write_file(GM_Globals[GM_LAST_UPDATE_CHECK_TXT], str(now_time), continue_on_error=True, display_errors=forceCheck) return announcement = release_data.get('body_text', 'No details about this release') - sys.stderr.write('\nGAM %s release notes:\n\n' % latest_version) + sys.stderr.write(f'\nGAM {latest_version} release notes:\n\n') sys.stderr.write(announcement) try: printLine(MESSAGE_HIT_CONTROL_C_TO_UPDATE) @@ -707,7 +704,7 @@ def getOSPlatform(): pltfrm = ' '.join([codename, mac_ver]) else: pltfrm = platform.platform() - return '%s %s' % (myos, pltfrm) + return f'{myos} {pltfrm}' def doGAMVersion(checkForArgs=True): force_check = extended = simple = timeOffset = False @@ -733,16 +730,21 @@ def doGAMVersion(checkForArgs=True): testLocation = sys.argv[i+1] i += 2 else: - controlflow.system_error_exit(2, '%s is not a valid argument for "gam version"' % sys.argv[i]) + controlflow.system_error_exit(2, f'{sys.argv[i]} is not a valid argument for "gam version"') if simple: sys.stdout.write(gam_version) return - version_data = 'GAM {0} - {1}\n{2}\nPython {3}.{4}.{5} {6}-bit {7}\ngoogle-api-python-client {8}\n{9} {10}\nPath: {11}' - print(version_data.format(gam_version, GAM_URL, gam_author, sys.version_info[0], - sys.version_info[1], sys.version_info[2], struct.calcsize('P')*8, - sys.version_info[3], googleapiclient.__version__, - getOSPlatform(), platform.machine(), GM_Globals[GM_GAM_PATH])) - if sys.platform.startswith('win') and str(struct.calcsize('P')*8).find('32') != -1 and platform.machine().find('64') != -1: + pyversion = platform.python_version() + cpu_bits = struct.calcsize('P') * 8 + print((f'GAM {gam_version} - {GAM_URL}\n' + f'{gam_author}\n' + f'Python {pyversion} {cpu_bits}-bit {sys.version_info.releaselevel}\n' + f'google-api-python-client {googleapiclient.__version__}\n' + f'{getOSPlatform()} {platform.machine()}\n' + f'Path: {GM_Globals[GM_GAM_PATH]}')) + if sys.platform.startswith('win') and \ + cpu_bits != 32 and \ + platform.machine().find('64') != -1: print(MESSAGE_UPDATE_GAM_TO_64BIT) if timeOffset: offset, nicetime = getLocalGoogleTimeOffset(testLocation) @@ -753,13 +755,13 @@ def doGAMVersion(checkForArgs=True): doGAMCheckForUpdates(forceCheck=True) if extended: print(ssl.OPENSSL_VERSION) - tls_ver, cipher_name = _getServerTLSUsed(testLocation) - print('%s connects using %s %s' % (testLocation, tls_ver, cipher_name)) + tls_ver, cipher_name, used_ip = _getServerTLSUsed(testLocation) + print(f'{testLocation} ({used_ip}) connects using {tls_ver} {cipher_name}') def _getServerTLSUsed(location): - url = 'https://%s' % location + url = f'https://{location}' _, netloc, _, _, _, _ = urlparse(url) - conn = 'https:%s' % netloc + conn = f'https:{netloc}' httpc = transport.create_http() headers = {'user-agent': GAM_INFO} retries = 5 @@ -774,7 +776,8 @@ def _getServerTLSUsed(location): continue controlflow.system_error_exit(4, str(e)) cipher_name, tls_ver, _ = httpc.connections[conn].sock.cipher() - return tls_ver, cipher_name + used_ip = httpc.connections[conn].sock.getpeername()[0] + return tls_ver, cipher_name, used_ip def _getSvcAcctData(): if not GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]: @@ -806,7 +809,7 @@ def getAPIVersion(api): return (api, version, '{0}-{1}'.format(api, version)) def readDiscoveryFile(api_version): - disc_filename = '%s.json' % (api_version) + disc_filename = f'{api_version}.json' disc_file = os.path.join(GM_Globals[GM_GAM_PATH], disc_filename) if hasattr(sys, '_MEIPASS'): pyinstaller_disc_file = os.path.join(sys._MEIPASS, disc_filename) @@ -841,7 +844,7 @@ def getValidOauth2TxtCredentials(force_refresh=False): """Gets OAuth2 credentials which are guaranteed to be fresh and valid. Locks during read and possible write so that only one process will attempt refresh/write when running in parallel. """ - lock_file = '%s.lock' % GC_Values[GC_OAUTH2_TXT] + lock_file = f'{GC_Values[GC_OAUTH2_TXT]}.lock' lock = FileLock(lock_file) with lock: credentials = getOauth2TxtStorageCredentials() @@ -1081,7 +1084,7 @@ def buildGmailGAPIObject(user): return (userEmail, buildGAPIServiceObject('gmail', userEmail)) def printPassFail(description, result): - print(' {0:74} {1}'.format(description, result)) + print(f' {description:74} {result}') def doCheckServiceAccount(users): i = 5 @@ -1092,7 +1095,7 @@ def doCheckServiceAccount(users): check_scopes = sys.argv[i+1].replace(',', ' ').split() i += 2 else: - controlflow.system_error_exit(3, '%s is not a valid argument for "gam user check serviceaccount"' % myarg) + controlflow.system_error_exit(3, f'{myarg} is not a valid argument for "gam user check serviceaccount"') print('Computer clock status:') timeOffset, nicetime = getLocalGoogleTimeOffset() if timeOffset < MAX_LOCAL_GOOGLE_TIME_OFFSET: @@ -1116,7 +1119,7 @@ def doCheckServiceAccount(users): except google.auth.exceptions.RefreshError as e: sa_token_result = 'FAIL' auth_error = str(e.args[0]) - printPassFail('Authenticating...%s' % auth_error, sa_token_result) + printPassFail(f'Authenticating...{auth_error}', sa_token_result) if not check_scopes: for _, scopes in list(API_SCOPE_MAPPING.items()): for scope in scopes: @@ -1127,7 +1130,7 @@ def doCheckServiceAccount(users): user = user.lower() all_scopes_pass = True oa2 = googleapiclient.discovery.build('oauth2', 'v1', transport.create_http()) - print('Domain-Wide Delegation authentication as %s:' % (user)) + print(f'Domain-Wide Delegation authentication as {user}:') for scope in check_scopes: # try with and without email scope for scopes in [[scope, USERINFO_EMAIL_SCOPE], [scope]]: @@ -1153,22 +1156,23 @@ def doCheckServiceAccount(users): printPassFail(scope, result) service_account = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] if all_scopes_pass: - print('\nAll scopes passed!\nService account %s is fully authorized.' % service_account) + print(f'\nAll scopes passed!\nService account {service_account} is fully authorized.') return user_domain = user[user.find('@')+1:] # Tack on email scope for more accurate checking check_scopes.append(USERINFO_EMAIL_SCOPE) - scopes_failed = '''Some scopes failed! Please go to: + nl = ',\n' + scopes_failed = f'''Some scopes failed! Please go to: -https://admin.google.com/%s/AdminHome?#OGX:ManageOauthClients +https://admin.google.com/{user_domain}/AdminHome?#OGX:ManageOauthClients and grant Client name: -%s +{service_account} Access to scopes: -%s\n''' % (user_domain, service_account, ',\n'.join(check_scopes)) +{nl.join(check_scopes)}{nl}''' controlflow.system_error_exit(1, scopes_failed) # Batch processing request_id fields @@ -1253,7 +1257,7 @@ def showReport(): to_drive = True i += 1 else: - controlflow.system_error_exit(2, '%s is not a valid argument to "gam report"' % sys.argv[i]) + controlflow.system_error_exit(2, f'{sys.argv[i]} is not a valid argument to "gam report"') if report in ['users', 'user']: while True: try: @@ -1295,7 +1299,7 @@ def showReport(): else: row[name] = '' csvRows.append(row) - writeCSVfile(csvRows, titles, 'User Reports - %s' % tryDate, to_drive) + writeCSVfile(csvRows, titles, f'User Reports - {tryDate}', to_drive) elif report in ['customer', 'customers', 'domain']: while True: try: @@ -1332,9 +1336,9 @@ def showReport(): app = {} for an_item in subitem: if an_item == 'client_name': - app['name'] = 'App: %s' % subitem[an_item].replace('\n', '\\n') + app['name'] = 'App: ' + subitem[an_item].replace('\n', '\\n') elif an_item == 'num_users': - app['value'] = '%s users' % subitem[an_item] + app['value'] = f'{subitem[an_item]} users' elif an_item == 'client_id': app['client_id'] = subitem[an_item] auth_apps.append(app) @@ -1349,17 +1353,17 @@ def showReport(): else: myvalue = value if mycount and myvalue: - values.append('%s:%s' % (myvalue, mycount)) + values.append(f'{myvalue}:{mycount}') value = ' '.join(values) elif 'version_number' in subitem and 'num_devices' in subitem: - values.append('%s:%s' % (subitem['version_number'], subitem['num_devices'])) + values.append(f'{subitem["version_number"]}:{subitem["num_devices"]}') else: continue value = ' '.join(sorted(values, reverse=True)) csvRows.append({'name': name, 'value': value}) for app in auth_apps: # put apps at bottom csvRows.append(app) - writeCSVfile(csvRows, titles, 'Customer Report - %s' % tryDate, todrive=to_drive) + writeCSVfile(csvRows, titles, f'Customer Report - {tryDate}', todrive=to_drive) else: if report in ['doc', 'docs']: report = 'drive' @@ -1426,7 +1430,7 @@ def showReport(): titles.append(item) csvRows.append(row) sortCSVTitles(['name',], titles) - writeCSVfile(csvRows, titles, '%s Activity Report' % report.capitalize(), to_drive) + writeCSVfile(csvRows, titles, f'{report.capitalize()} Activity Report', to_drive) def watchGmail(users): project = 'projects/{0}'.format(_getCurrentProjectID()) @@ -1736,11 +1740,11 @@ def doGetDomainAliasInfo(): def doGetCustomerInfo(): cd = buildGAPIObject('directory') customer_info = gapi.call(cd.customers(), 'get', customerKey=GC_Values[GC_CUSTOMER_ID]) - print('Customer ID: %s' % customer_info['id']) - print('Primary Domain: %s' % customer_info['customerDomain']) + print(f'Customer ID: {customer_info["id"]}') + print(f'Primary Domain: {customer_info["customerDomain"]}') result = gapi.call(cd.domains(), 'get', customer=customer_info['id'], domainName=customer_info['customerDomain'], fields='verified') - print('Primary Domain Verified: %s' % result['verified']) + print(f'Primary Domain Verified: {result["verified"]}') # If customer has changed primary domain customerCreationTime is date # of current primary being added, not customer create date. # We should also get all domains and use oldest date @@ -1752,16 +1756,17 @@ def doGetCustomerInfo(): domain_creation = datetime.datetime.fromtimestamp(int(domain['creationTime'])/1000) if domain_creation < oldest: oldest = domain_creation - print('Customer Creation Time: %s' % oldest.strftime('%Y-%m-%dT%H:%M:%SZ')) - print('Default Language: %s' % customer_info.get('language', 'Unset (defaults to en)')) + print(f'Customer Creation Time: {oldest.strftime("%Y-%m-%dT%H:%M:%SZ")}') + customer_language = customer_info.get('language', 'Unset (defaults to en)') + print(f'Default Language: {customer_language}') if 'postalAddress' in customer_info: print('Address:') for field in ADDRESS_FIELDS_PRINT_ORDER: if field in customer_info['postalAddress']: - print(' %s: %s' % (field, customer_info['postalAddress'][field])) + print(f' {field}: {customer_info["postalAddress"][field]}') if 'phoneNumber' in customer_info: - print('Phone: %s' % customer_info['phoneNumber']) - print('Admin Secondary Email: %s' % customer_info['alternateEmail']) + print(f'Phone: {customer_info["phoneNumber"]}') + print(f'Admin Secondary Email: {customer_info["alternateEmail"]}') user_counts_map = { 'accounts:num_users': 'Total Users', 'accounts:gsuite_basic_total_licenses': 'G Suite Basic Licenses', @@ -1788,12 +1793,12 @@ def doGetCustomerInfo(): if not usage: print('No user count data available.') return - print('User counts as of %s:' % tryDate) + print(f'User counts as of {tryDate}:') for item in usage[0]['parameters']: api_name = user_counts_map.get(item['name']) api_value = int(item.get('intValue', 0)) if api_name and api_value: - print(' {}: {:,}'.format(api_name, api_value)) + print(f' {api_name}: {api_value:,}') def doUpdateCustomer(): cd = buildGAPIObject('directory') @@ -1815,7 +1820,7 @@ def doUpdateCustomer(): body['language'] = sys.argv[i+1] i += 2 else: - controlflow.system_error_exit(2, '%s is not a valid argument for "gam update customer"' % myarg) + controlflow.system_error_exit(2, f'{myarg} is not a valid argument for "gam update customer"') if not body: controlflow.system_error_exit(2, 'no arguments specified for "gam update customer"') gapi.call(cd.customers(), 'patch', customerKey=GC_Values[GC_CUSTOMER_ID], body=body) @@ -1843,7 +1848,7 @@ def doPrintDomains(): todrive = True i += 1 else: - controlflow.system_error_exit(2, '%s is not a valid argument for "gam print domains".' % sys.argv[i]) + controlflow.system_error_exit(2, f'{sys.argv[i]} is not a valid argument for "gam print domains".') results = gapi.call(cd.domains(), 'list', customer=GC_Values[GC_CUSTOMER_ID]) for domain in results['domains']: domain_attributes = {} @@ -1886,7 +1891,7 @@ def doPrintDomainAliases(): todrive = True i += 1 else: - controlflow.system_error_exit(2, '%s is not a valid argument for "gam print domainaliases".' % sys.argv[i]) + controlflow.system_error_exit(2, f'{sys.argv[i]} is not a valid argument for "gam print domainaliases".') results = gapi.call(cd.domainAliases(), 'list', customer=GC_Values[GC_CUSTOMER_ID]) for domainAlias in results['domainAliases']: domainAlias_attributes = {} @@ -1904,7 +1909,7 @@ def doPrintDomainAliases(): def doDelAdmin(): cd = buildGAPIObject('directory') roleAssignmentId = sys.argv[3] - print('Deleting Admin Role Assignment %s' % roleAssignmentId) + print(f'Deleting Admin Role Assignment {roleAssignmentId}') gapi.call(cd.roleAssignments(), 'delete', customer=GC_Values[GC_CUSTOMER_ID], roleAssignmentId=roleAssignmentId) @@ -1916,14 +1921,14 @@ def doCreateAdmin(): body['roleId'] = getRoleId(role) body['scopeType'] = sys.argv[5].upper() if body['scopeType'] not in ['CUSTOMER', 'ORG_UNIT']: - controlflow.system_error_exit(3, 'scope type must be customer or org_unit; got %s' % body['scopeType']) + controlflow.system_error_exit(3, f'scope type must be customer or org_unit; got {body["scopeType"]}') if body['scopeType'] == 'ORG_UNIT': orgUnit, orgUnitId = getOrgUnitId(sys.argv[6], cd) body['orgUnitId'] = orgUnitId[3:] scope = 'ORG_UNIT {0}'.format(orgUnit) else: scope = 'CUSTOMER' - print('Giving %s admin role %s for %s' % (user, role, scope)) + print(f'Giving {user} admin role {role} for {scope}') gapi.call(cd.roleAssignments(), 'insert', customer=GC_Values[GC_CUSTOMER_ID], body=body) @@ -1931,7 +1936,7 @@ def doPrintAdminRoles(): cd = buildGAPIObject('directory') todrive = False titles = ['roleId', 'roleName', 'roleDescription', 'isSuperAdminRole', 'isSystemRole'] - fields = 'nextPageToken,items({0})'.format(','.join(titles)) + fields = f'nextPageToken,items({",".join(titles)})' csvRows = [] i = 3 while i < len(sys.argv): @@ -1940,7 +1945,7 @@ def doPrintAdminRoles(): todrive = True i += 1 else: - controlflow.system_error_exit(2, '%s is not a valid argument for "gam print adminroles".' % sys.argv[i]) + controlflow.system_error_exit(2, f'{sys.argv[i]} is not a valid argument for "gam print adminroles".') roles = gapi.get_all_pages(cd.roles(), 'list', 'items', customer=GC_Values[GC_CUSTOMER_ID], fields=fields) for role in roles: @@ -1955,7 +1960,7 @@ def doPrintAdmins(): roleId = None userKey = None todrive = False - fields = 'nextPageToken,items({0})'.format(','.join(['roleAssignmentId', 'roleId', 'assignedTo', 'scopeType', 'orgUnitId'])) + fields = 'nextPageToken,items(roleAssignmentId,roleId,assignedTo,scopeType,orgUnitId)' titles = ['roleAssignmentId', 'roleId', 'role', 'assignedTo', 'assignedToUser', 'scopeType', 'orgUnitId', 'orgUnit'] csvRows = [] i = 3 @@ -1971,7 +1976,7 @@ def doPrintAdmins(): todrive = True i += 1 else: - controlflow.system_error_exit(2, '%s is not a valid argument for "gam print admins".' % sys.argv[i]) + controlflow.system_error_exit(2, f'{sys.argv[i]} is not a valid argument for "gam print admins".') admins = gapi.get_all_pages(cd.roleAssignments(), 'list', 'items', customer=GC_Values[GC_CUSTOMER_ID], userKey=userKey, roleId=roleId, fields=fields) for admin in admins: @@ -2030,7 +2035,7 @@ def getRoleId(role): else: roleId = roleid_from_role(role) if not roleId: - controlflow.system_error_exit(4, '%s is not a valid role. Please ensure role name is exactly as shown in admin console.' % role) + controlflow.system_error_exit(4, f'{role} is not a valid role. Please ensure role name is exactly as shown in admin console.') return roleId def buildUserIdToNameMap(): @@ -2055,7 +2060,7 @@ def appID2app(dt, appID): for online_service in online_services: if appID == online_service['id']: return online_service['name'] - return 'applicationId: {0}'.format(appID) + return f'applicationId: {appID}' def app2appID(dt, app): serviceName = app.lower() @@ -2065,7 +2070,7 @@ def app2appID(dt, app): for online_service in online_services: if serviceName == online_service['name'].lower(): return (online_service['name'], online_service['id']) - controlflow.system_error_exit(2, '%s is not a valid service for data transfer.' % app) + controlflow.system_error_exit(2, f'{app} is not a valid service for data transfer.') def convertToUserID(user): cg = UID_PATTERN.match(user) @@ -2073,18 +2078,18 @@ def convertToUserID(user): return cg.group(1) cd = buildGAPIObject('directory') if user.find('@') == -1: - user = '%s@%s' % (user, GC_Values[GC_DOMAIN]) + user = f'{user}@{GC_Values[GC_DOMAIN]}' try: return gapi.call(cd.users(), 'get', throw_reasons=[gapi.errors.ErrorReason.USER_NOT_FOUND, gapi.errors.ErrorReason.BAD_REQUEST, gapi.errors.ErrorReason.FORBIDDEN], userKey=user, fields='id')['id'] except (gapi.errors.GapiUserNotFoundError, gapi.errors.GapiBadRequestError, gapi.errors.GapiForbiddenError): - controlflow.system_error_exit(3, 'no such user %s' % user) + controlflow.system_error_exit(3, f'no such user {user}') def convertUserIDtoEmail(uid): cd = buildGAPIObject('directory') try: return gapi.call(cd.users(), 'get', throw_reasons=[gapi.errors.ErrorReason.USER_NOT_FOUND, gapi.errors.ErrorReason.BAD_REQUEST, gapi.errors.ErrorReason.FORBIDDEN], userKey=uid, fields='primaryEmail')['primaryEmail'] except (gapi.errors.GapiUserNotFoundError, gapi.errors.GapiBadRequestError, gapi.errors.GapiForbiddenError): - return 'uid:{0}'.format(uid) + return f'uid:{uid}' def doCreateDataTransfer(): dt = buildGAPIObject('datatransfer') @@ -2114,7 +2119,7 @@ def doCreateDataTransfer(): body['applicationDataTransfers'][i]['applicationTransferParams'].append({'key': key, 'value': value}) i += 1 result = gapi.call(dt.transfers(), 'insert', body=body, fields='id')['id'] - print('Submitted request id %s to transfer %s from %s to %s' % (result, ','.join(map(str, appNameList)), old_owner, new_owner)) + print(f'Submitted request id {result} to transfer {",".join(map(str, appNameList))} from {old_owner} to {new_owner}') def doPrintTransferApps(): dt = buildGAPIObject('datatransfer') @@ -2147,7 +2152,7 @@ def doPrintDataTransfers(): todrive = True i += 1 else: - controlflow.system_error_exit(2, '%s is not a valid argument for "gam print transfers"' % sys.argv[i]) + controlflow.system_error_exit(2, f'{sys.argv[i]} is not a valid argument for "gam print transfers"') transfers = gapi.get_all_pages(dt.transfers(), 'list', 'dataTransfers', customerId=GC_Values[GC_CUSTOMER_ID], status=status, newOwnerUserId=newOwnerUserId, oldOwnerUserId=oldOwnerUserId) @@ -2174,16 +2179,16 @@ def doGetDataTransferInfo(): dt = buildGAPIObject('datatransfer') dtId = sys.argv[3] transfer = gapi.call(dt.transfers(), 'get', dataTransferId=dtId) - print('Old Owner: %s' % convertUserIDtoEmail(transfer['oldOwnerUserId'])) - print('New Owner: %s' % convertUserIDtoEmail(transfer['newOwnerUserId'])) - print('Request Time: %s' % transfer['requestTime']) + print(f'Old Owner: {convertUserIDtoEmail(transfer["oldOwnerUserId"])}') + print(f'New Owner: {convertUserIDtoEmail(transfer["newOwnerUserId"])}') + print(f'Request Time: {transfer["requestTime"]}') for app in transfer['applicationDataTransfers']: - print('Application: %s' % appID2app(dt, app['applicationId'])) - print('Status: %s' % app['applicationTransferStatus']) + print(f'Application: {appID2app(dt, app["applicationId"])}') + print(f'Status: {app["applicationTransferStatus"]}') print('Parameters:') if 'applicationTransferParams' in app: for param in app['applicationTransferParams']: - print(' %s: %s' % (param['key'], ','.join(param.get('value', [])))) + print(f' {param["key"]}: {",".join(param.get("value", []))}') else: print(' None') print() @@ -4655,9 +4660,10 @@ def createDriveFile(users): csv_rows.append({'User': user, 'title': result['title'], 'id': result['id']}) else: if parameters[DFA_LOCALFILENAME]: - print('Successfully uploaded %s to Drive File %s' % (parameters[DFA_LOCALFILENAME], titleInfo)) + print(f'Successfully uploaded {parameters[DFA_LOCALFILENAME]} to Drive File {titleInfo}') else: - print('Successfully created Drive %s %s' % (['Folder', 'File'][result['mimeType'] != MIMETYPE_GA_FOLDER], titleInfo)) + created_type = ['Folder', 'File'][result['mimeType'] != MIMETYPE_GA_FOLDER] + print(f'Successfully created Drive {created_type} {titleInfo}') if csv_output: writeCSVfile(csv_rows, csv_titles, 'Files', to_drive) @@ -5994,13 +6000,14 @@ def renameLabels(users): merge = True i += 1 else: - controlflow.system_error_exit(2, '%s is not a valid argument for "gam rename label"' % sys.argv[i]) + controlflow.system_error_exit(2, f'{sys.argv[i]} is not a valid argument for "gam rename label"') pattern = re.compile(search, re.IGNORECASE) for user in users: user, gmail = buildGmailGAPIObject(user) if not gmail: continue labels = gapi.call(gmail.users().labels(), 'list', userId=user) + print(f'got {len(labels["labels"])} labels') for label in labels['labels']: if label['type'] == 'system': continue @@ -6009,15 +6016,15 @@ def renameLabels(users): try: new_label_name = replace % match_result.groups() except TypeError: - controlflow.system_error_exit(2, 'The number of subfields ({0}) in search "{1}" does not match the number of subfields ({2}) in replace "{3}"'.format(len(match_result.groups()), search, replace.count('%s'), replace)) - print(' Renaming "%s" to "%s"' % (label['name'], new_label_name)) + controlflow.system_error_exit(2, f'The number of subfields ({len(match_result.groups())}) in search "{search}" does not match the number of subfields ({replace.count("%s")}) in replace "{replace}"') + print(f' Renaming "{label["name"]}" to "{new_label_name}"') try: gapi.call(gmail.users().labels(), 'patch', soft_errors=True, throw_reasons=[gapi.errors.ErrorReason.ABORTED], id=label['id'], userId=user, body={'name': new_label_name}) except gapi.errors.GapiAbortedError: if merge: - print(' Merging %s label to existing %s label' % (label['name'], new_label_name)) + print(f' Merging {label["name"]} label to existing {new_label_name} label') messages_to_relabel = gapi.get_all_pages(gmail.users().messages(), 'list', 'messages', - userId=user, q='label:%s' % cleanLabelQuery(label['name'])) + userId=user, q=f'label:{cleanLabelQuery(label["name"])}') if messages_to_relabel: for new_label in labels['labels']: if new_label['name'].lower() == new_label_name.lower(): @@ -10478,26 +10485,26 @@ def print_json(object_name, object_value, spacing=''): if object_name in ['kind', 'etag', 'etags']: return if object_name is not None: - sys.stdout.write('%s%s: ' % (spacing, object_name)) + sys.stdout.write(f'{spacing}{object_name}: ') if isinstance(object_value, list): if len(object_value) == 1 and isinstance(object_value[0], (str, int, bool)): - sys.stdout.write('%s\n' % object_value[0]) + sys.stdout.write(f'{object_value[0]}\n') return if object_name is not None: sys.stdout.write('\n') for a_value in object_value: if isinstance(a_value, (str, int, bool)): - sys.stdout.write(' %s%s\n' % (spacing, a_value)) + sys.stdout.write(f' {spacing}{a_value}\n') else: - print_json(None, a_value, ' %s' % spacing) + print_json(None, a_value, f' {spacing}') elif isinstance(object_value, dict): print() if object_name is not None: sys.stdout.write('\n') for another_object in object_value: - print_json(another_object, object_value[another_object], ' %s' % spacing) + print_json(another_object, object_value[another_object], f' {spacing}') else: - sys.stdout.write('%s\n' % (object_value)) + sys.stdout.write(f'{object_value}\n') def doSiteVerifyShow(): verif = buildGAPIObject('siteVerification') @@ -10576,7 +10583,7 @@ def doSiteVerifyAttempt(): query_params['type'] = 'txt' full_url = base_url + urlencode(query_params) (_, c) = simplehttp.request(full_url, 'GET') - result = json.loads(c.decode(UTF8)) + result = json.loads(c) status = result['Status'] if status == 0 and 'Answer' in result: answers = result['Answer'] @@ -13126,29 +13133,30 @@ def OAuthInfo(): show_secret = True i += 1 else: - controlflow.system_error_exit(3, '%s is not a valid argument to "gam oauth info"' % sys.argv[i]) + controlflow.system_error_exit(3, f'{sys.argv[i]} is not a valid argument to "gam oauth info"') if not access_token and not id_token: credentials = getValidOauth2TxtCredentials() access_token = credentials.token - print("\nOAuth File: %s" % GC_Values[GC_OAUTH2_TXT]) + print(f'\nOAuth File: {GC_Values[GC_OAUTH2_TXT]}') oa2 = buildGAPIObject('oauth2') token_info = gapi.call(oa2, 'tokeninfo', access_token=access_token, id_token=id_token) if 'issued_to' in token_info: - print('Client ID: %s' % token_info['issued_to']) + print(f'Client ID: {token_info["issued_to"]}') if credentials is not None and show_secret: - print("Secret: %s" % credentials.client_secret) + print(f'Secret: {credentials.client_secret}') if 'scope' in token_info: scopes = token_info['scope'].split(' ') - print('Scopes (%s):' % len(scopes)) + print(f'Scopes ({len(scopes)})') for scope in sorted(scopes): - print(' %s' % scope) + print(f' {scope}') if 'email' in token_info: - print('G Suite Admin: %s' % token_info['email']) + print(f'G Suite Admin: {token_info["email"]}') if 'expires_in' in token_info: - print('Expires: %s' % (datetime.datetime.now()+datetime.timedelta(seconds=token_info['expires_in'])).isoformat()) + expires = (datetime.datetime.now() + datetime.timedelta(seconds=token_info['expires_in'])).isoformat() + print(f'Expires: {expires}') for key, value in token_info.items(): if key not in ['issued_to', 'scope', 'email', 'expires_in']: - print('%s: %s' % (key, value)) + print(f'{key}: {value}') def doDeleteOAuth(): lock_file = '%s.lock' % GC_Values[GC_OAUTH2_TXT] @@ -13159,7 +13167,7 @@ def doDeleteOAuth(): return simplehttp = transport.create_http() params = {'token': credentials.refresh_token} - revoke_uri = 'https://accounts.google.com/o/oauth2/revoke?%s' % urlencode(params) + revoke_uri = f'https://accounts.google.com/o/oauth2/revoke?{urlencode(params)}' sys.stderr.write('This OAuth token will self-destruct in 3...') sys.stderr.flush() time.sleep(1) @@ -13192,7 +13200,7 @@ def writeCredentials(creds): } expected_iss = ['https://accounts.google.com', 'accounts.google.com'] if _getValueFromOAuth('iss', creds) not in expected_iss: - controlflow.system_error_exit(13, 'Wrong OAuth 2.0 credentials issuer. Got %s, expected one of %s' % (_getValueFromOAuth('iss', creds), ', '.join(expected_iss))) + controlflow.system_error_exit(13, f'Wrong OAuth 2.0 credentials issuer. Got {_getValueFromOAuth("iss", creds)} expected one of {", ".join(expected_iss)}') creds_data['decoded_id_token'] = GC_Values[GC_DECODED_ID_TOKEN] data = json.dumps(creds_data, indent=2, sort_keys=True) fileutils.write_file(GC_Values[GC_OAUTH2_TXT], data) @@ -13210,7 +13218,7 @@ def doRequestOAuth(login_hint=None): creds = _run_oauth_flow(client_id, client_secret, scopes, 'offline', login_hint) writeCredentials(creds) else: - print('It looks like you\'ve already authorized GAM. Refusing to overwrite existing file:\n\n%s' % GC_Values[GC_OAUTH2_TXT]) + print(f'It looks like you\'ve already authorized GAM. Refusing to overwrite existing file:\n\n{GC_Values[GC_OAUTH2_TXT]}') def getOAuthClientIDAndSecret(): """Retrieves the OAuth client ID and client secret from JSON.""" @@ -13230,8 +13238,8 @@ gam create project client_id = re.sub(r'\.apps\.googleusercontent\.com$', '', client_id) client_secret = cs_json['installed']['client_secret'] except (ValueError, IndexError, KeyError): - controlflow.system_error_exit(3, 'the format of your client secrets file:\n\n%s\n\n' - 'is incorrect. Please recreate the file.' % filename) + controlflow.system_error_exit(3, f'the format of your client secrets file:\n\n{filename}\n\n' + 'is incorrect. Please recreate the file.') return (client_id, client_secret) OAUTH2_SCOPES = [ @@ -13444,10 +13452,10 @@ class ScopeMenuOption(): if self.supports_restriction(restriction): self._restriction = restriction else: - error = 'Scope does not support a %s restriction.' % restriction + error = f'Scope does not support a {restriction} restriction.' if self.supported_restrictions is not None: restriction_list = ', '.join(self.supported_restrictions) - error = error + (' Supported restrictions are: %s' % restriction_list) + error = error + (f' Supported restrictions are: {restriction_list}') raise ValueError(error) def unrestrict(self): @@ -13463,7 +13471,7 @@ class ScopeMenuOption(): effective_scopes = [] for scope in self.scopes: if self.is_restricted: - scope = '%s.%s' % (scope, self._restriction) + scope = f'{scope}.{self._restriction}' effective_scopes.append(scope) return effective_scopes @@ -13614,15 +13622,12 @@ Append an 'r' to grant read-only access or an 'a' to grant action-only access. else: indicator = SELECTION_INDICATOR['ALL_SELECTED'] - item_description = [ - '[%s]' % indicator, - '%2d)' % option_number, - scope_option.description, - ] + item_description = [f'[{indicator}]', f'{option_number:2d})', + scope_option.description,] if scope_option.supported_restrictions: - item_description.append( - '(supports %s)' % ' and '.join(scope_option.supported_restrictions)) + restrictions = ' and '.join(scope_option.supported_restrictions) + item_description.append(f'(supports {restrictions})') if scope_option.is_required: item_description.append('[required]') @@ -13713,7 +13718,7 @@ Append an 'r' to grant read-only access or an 'a' to grant action-only access. # to the indices in the list of scopes. if scope_number < 0 or scope_number > len(self._options) - 1: raise ScopeSelectionMenu.MenuChoiceError( - 'Invalid scope number "%d"' % scope_number) + f'Invalid scope number "{scope_number}"') selected_option = self._options[scope_number] # Find the restriction that the user intended to apply. @@ -13721,8 +13726,7 @@ Append an 'r' to grant read-only access or an 'a' to grant action-only access. matching_restrictions = [r for r in selected_option.supported_restrictions if r.startswith(restriction_command)] if not matching_restrictions: raise ScopeSelectionMenu.MenuChoiceError( - 'Scope "%s" does not support "%s" mode!' % ( - selected_option.description, restriction_command)) + f'Scope "{selected_option.description}" does not support "{restriction_command}" mode!') restriction = matching_restrictions[0] else: restriction = None @@ -13741,7 +13745,7 @@ Append an 'r' to grant read-only access or an 'a' to grant action-only access. raise ScopeSelectionMenu.UserRequestedExitException() else: raise ScopeSelectionMenu.MenuChoiceError( - 'Invalid input "%s"' % user_input) + f'Invalid input "{user_input}"') return True @@ -13760,8 +13764,7 @@ Append an 'r' to grant read-only access or an 'a' to grant action-only access. """ if option.is_required and (not selected or selected is None): raise ScopeSelectionMenu.MenuChoiceError( - 'Scope "%s" is required and cannot be unselected!' % - option.description) + f'Scope "{option.description}" is required and cannot be unselected!') if selected and not option.is_selected: # Make sure we're not about to exceed the maximum number of scopes # authorized on a single token. @@ -13770,9 +13773,9 @@ Append an 'r' to grant read-only access or an 'a' to grant action-only access. expected_num_scopes = num_scopes_to_add + num_selected_scopes if expected_num_scopes > ScopeSelectionMenu.MAXIMUM_NUM_SCOPES: raise ScopeSelectionMenu.MenuChoiceError( - 'Too many scopes selected (%d). Maximum is %d. Please remove some ' - 'scopes and try again.' % ( - expected_num_scopes, ScopeSelectionMenu.MAXIMUM_NUM_SCOPES)) + f'Too many scopes selected ({expected_num_scopes}). Maximum is ' + f'{ScopeSelectionMenu.MAXIMUM_NUM_SCOPES}.Please remove some scopes ' + 'and try again.') if restriction is None: if selected is None: @@ -13785,8 +13788,7 @@ Append an 'r' to grant read-only access or an 'a' to grant action-only access. option.select(restriction) else: raise ScopeSelectionMenu.MenuChoiceError( - 'Scope "%s" does not support %s mode!' % ( - option.description, restriction)) + f'Scope "{option.description}" does not support {restriction} mode!') def init_gam_worker(): signal.signal(signal.SIGINT, signal.SIG_IGN) @@ -14717,6 +14719,6 @@ if __name__ == "__main__": # to break parallel operations with errors about extra -b # command line arguments mp_set_start_method('fork') - if sys.version_info[0] < 3 or sys.version_info[1] < 5: - controlflow.system_error_exit(5, 'GAM requires Python 3.5 or newer. You are running %s.%s.%s. Please upgrade your Python version or use one of the binary GAM downloads.' % sys.version_info[:3]) + if sys.version_info[0] < 3 or sys.version_info[1] < 6: + controlflow.system_error_exit(5, f'GAM requires Python 3.6 or newer. You are running %s.%s.%s. Please upgrade your Python version or use one of the binary GAM downloads.' % sys.version_info[:3]) sys.exit(ProcessGAMCommand(sys.argv)) diff --git a/src/var.py b/src/var.py index 209c76b6..be2fac09 100644 --- a/src/var.py +++ b/src/var.py @@ -10,11 +10,9 @@ gam_version = '4.97' gam_license = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' GAM_URL = 'https://git.io/gam' -GAM_INFO = 'GAM {0} - {1} / {2} / Python {3}.{4}.{5} {6} / {7} {8} /'.format(gam_version, GAM_URL, - gam_author, - sys.version_info[0], sys.version_info[1], - sys.version_info[2], sys.version_info[3], - platform.platform(), platform.machine()) +GAM_INFO = (f'GAM {gam_version} - {GAM_URL} / {gam_author} / ' + f'Python {platform.python_version()} {sys.version_info.releaselevel} / ' + f'{platform.platform()} {platform.machine()}') GAM_RELEASES = 'https://github.com/jay0lee/GAM/releases' GAM_WIKI = 'https://github.com/jay0lee/GAM/wiki'