This commit is contained in:
Jay Lee
2020-05-12 14:23:29 -04:00
parent 8eb72ae6e7
commit d46dd46732
3 changed files with 120 additions and 91 deletions

View File

@ -210,8 +210,8 @@ script:
- if [ "$e2e" = true ]; then $gam print cros allfields nolists; fi - if [ "$e2e" = true ]; then $gam print cros allfields nolists; fi
- if [ "$e2e" = true ]; then $gam report usageparameters customer; fi - if [ "$e2e" = true ]; then $gam report usageparameters customer; fi
- if [ "$e2e" = true ]; then $gam report usage customer parameters gmail:num_emails_sent,accounts:num_1day_logins; fi - if [ "$e2e" = true ]; then $gam report usage customer parameters gmail:num_emails_sent,accounts:num_1day_logins; fi
- if [ "$e2e" = true ]; then $gam report customer date -5d todrive; fi - if [ "$e2e" = true ]; then $gam report customer todrive; fi
- if [ "$e2e" = true ]; then $gam report users date -5d fields accounts:is_less_secure_apps_access_allowed,gmail:last_imap_time,gmail:last_pop_time filters "accounts:last_login_time>2019-01-01T00:00:00.000Z" todrive; fi - if [ "$e2e" = true ]; then $gam report users fields accounts:is_less_secure_apps_access_allowed,gmail:last_imap_time,gmail:last_pop_time filters "accounts:last_login_time>2019-01-01T00:00:00.000Z" todrive; fi
- if [ "$e2e" = true ]; then $gam report admin start -3d todrive; fi - if [ "$e2e" = true ]; then $gam report admin start -3d todrive; fi
- if ([ "$e2e" = true ] && [[ "$TRAVIS_JOB_NAME" != *"Testing" ]]); then - if ([ "$e2e" = true ] && [[ "$TRAVIS_JOB_NAME" != *"Testing" ]]); then
for gamfile in gam-$GAMVERSION-*; do for gamfile in gam-$GAMVERSION-*; do

View File

@ -602,7 +602,7 @@ def doGAMCheckForUpdates(forceCheck=False):
controlflow.system_error_exit( controlflow.system_error_exit(
4, 'GAM Latest Version information not available') 4, 'GAM Latest Version information not available')
current_version = gam_version current_version = GAM_VERSION
now_time = int(time.time()) now_time = int(time.time())
if forceCheck: if forceCheck:
check_url = GAM_ALL_RELEASES # includes pre-releases check_url = GAM_ALL_RELEASES # includes pre-releases
@ -709,15 +709,15 @@ def doGAMVersion(checkForArgs=True):
else: else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam version') controlflow.invalid_argument_exit(sys.argv[i], 'gam version')
if simple: if simple:
sys.stdout.write(gam_version) sys.stdout.write(GAM_VERSION)
return return
pyversion = platform.python_version() pyversion = platform.python_version()
cpu_bits = struct.calcsize('P') * 8 cpu_bits = struct.calcsize('P') * 8
api_client_ver = pkg_resources.get_distribution( api_client_ver = pkg_resources.get_distribution(
'google-api-python-client').version 'google-api-python-client').version
print( print(
(f'GAM {gam_version} - {GAM_URL} - {GM_Globals[GM_GAM_TYPE]}\n' (f'GAM {GAM_VERSION} - {GAM_URL} - {GM_Globals[GM_GAM_TYPE]}\n'
f'{gam_author}\n' f'{GAM_AUTHOR}\n'
f'Python {pyversion} {cpu_bits}-bit {sys.version_info.releaselevel}\n' f'Python {pyversion} {cpu_bits}-bit {sys.version_info.releaselevel}\n'
f'google-api-python-client {api_client_ver}\n' f'google-api-python-client {api_client_ver}\n'
f'{getOSPlatform()} {platform.machine()}\n' f'{getOSPlatform()} {platform.machine()}\n'
@ -3296,7 +3296,7 @@ def doPrinterRegister():
'uuid': 'uuid':
_getValueFromOAuth('sub'), _getValueFromOAuth('sub'),
'manufacturer': 'manufacturer':
gam_author, GAM_AUTHOR,
'model': 'model':
'cp1', 'cp1',
'gcp_version': 'gcp_version':
@ -3308,7 +3308,7 @@ def doPrinterRegister():
'update_url': 'update_url':
GAM_RELEASES, GAM_RELEASES,
'firmware': 'firmware':
gam_version, GAM_VERSION,
'semantic_state': { 'semantic_state': {
'version': '1.0', 'version': '1.0',
'printer': { 'printer': {
@ -8609,9 +8609,10 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
name, local_key_size) name, local_key_size)
print(' Uploading new public certificate to Google...') print(' Uploading new public certificate to Google...')
max_retries = 10 max_retries = 10
for i in range(1, max_retries+1): for i in range(1, max_retries + 1):
try: try:
result = gapi.call(iam.projects().serviceAccounts().keys(), result = gapi.call(
iam.projects().serviceAccounts().keys(),
'upload', 'upload',
throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND], throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND],
name=name, name=name,
@ -8620,9 +8621,11 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
except gapi_errors.GapiNotFoundError as e: except gapi_errors.GapiNotFoundError as e:
if i == max_retries: if i == max_retries:
raise e raise e
sleep_time = i*5 sleep_time = i * 5
if i > 3: if i > 3:
print(f'Waiting for Service Account creation to complete. Sleeping {sleep_time} seconds\n') print(
f'Waiting for Service Account creation to complete. Sleeping {sleep_time} seconds\n'
)
time.sleep(sleep_time) time.sleep(sleep_time)
private_key_id = result['name'].rsplit('/', 1)[-1] private_key_id = result['name'].rsplit('/', 1)[-1]
oauth2service_data = _formatOAuth2ServiceData(project_id, client_email, oauth2service_data = _formatOAuth2ServiceData(project_id, client_email,
@ -9894,8 +9897,8 @@ def doWhatIs():
], ],
userKey=email, userKey=email,
fields='id,primaryEmail') fields='id,primaryEmail')
if (user_or_alias['primaryEmail'].lower() == email) or ( if (user_or_alias['primaryEmail'].lower()
user_or_alias['id'] == email): == email) or (user_or_alias['id'] == email):
sys.stderr.write(f'{email} is a user\n\n') sys.stderr.write(f'{email} is a user\n\n')
doGetUserInfo(user_email=email) doGetUserInfo(user_email=email)
return return
@ -12915,9 +12918,8 @@ def getUsersToModify(entity_type=None,
query=query) query=query)
for member in members: for member in members:
email = member['primaryEmail'] email = member['primaryEmail']
if (checkSuspended is None or if (checkSuspended is None or checkSuspended
checkSuspended == member['suspended'] == member['suspended']) and email not in usersSet:
) and email not in usersSet:
usersSet.add(email) usersSet.add(email)
users.append(email) users.append(email)
if not silent: if not silent:

View File

@ -1,3 +1,4 @@
"""Variables common across modules"""
import os import os
import ssl import ssl
import string import string
@ -5,13 +6,13 @@ import sys
import platform import platform
import re import re
gam_author = 'Jay Lee <jay0lee@gmail.com>' GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
gam_version = '5.08' GAM_VERSION = '5.09'
gam_license = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam' GAM_URL = 'https://git.io/gam'
GAM_INFO = ( GAM_INFO = (
f'GAM {gam_version} - {GAM_URL} / {gam_author} / ' f'GAM {GAM_VERSION} - {GAM_URL} / {GAM_AUTHOR} / '
f'Python {platform.python_version()} {sys.version_info.releaselevel} / ' f'Python {platform.python_version()} {sys.version_info.releaselevel} / '
f'{platform.platform()} {platform.machine()}') f'{platform.platform()} {platform.machine()}')
@ -930,7 +931,12 @@ CROS_DISK_VOLUME_REPORTS_ARGUMENTS = [
CROS_SYSTEM_RAM_FREE_REPORTS_ARGUMENTS = [ CROS_SYSTEM_RAM_FREE_REPORTS_ARGUMENTS = [
'systemramfreereports', 'systemramfreereports',
] ]
CROS_LISTS_ARGUMENTS = CROS_ACTIVE_TIME_RANGES_ARGUMENTS + CROS_RECENT_USERS_ARGUMENTS + CROS_DEVICE_FILES_ARGUMENTS + CROS_CPU_STATUS_REPORTS_ARGUMENTS + CROS_DISK_VOLUME_REPORTS_ARGUMENTS + CROS_SYSTEM_RAM_FREE_REPORTS_ARGUMENTS CROS_LISTS_ARGUMENTS = CROS_ACTIVE_TIME_RANGES_ARGUMENTS + \
CROS_RECENT_USERS_ARGUMENTS + \
CROS_DEVICE_FILES_ARGUMENTS + \
CROS_CPU_STATUS_REPORTS_ARGUMENTS + \
CROS_DISK_VOLUME_REPORTS_ARGUMENTS + \
CROS_SYSTEM_RAM_FREE_REPORTS_ARGUMENTS
CROS_START_ARGUMENTS = ['start', 'startdate', 'oldestdate'] CROS_START_ARGUMENTS = ['start', 'startdate', 'oldestdate']
CROS_END_ARGUMENTS = ['end', 'enddate'] CROS_END_ARGUMENTS = ['end', 'enddate']
@ -1097,7 +1103,8 @@ GM_Globals = {
# #
# Global variables defined by environment variables/signal files # Global variables defined by environment variables/signal files
# #
# 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, don't automatically generate gam batch commands # Default: 0, don't automatically generate gam batch commands
GC_AUTO_BATCH_MIN = 'auto_batch_min' GC_AUTO_BATCH_MIN = 'auto_batch_min'
# When processing items in batches, how many should be processed in each batch # When processing items in batches, how many should be processed in each batch
@ -1110,20 +1117,24 @@ GC_CACHE_DISCOVERY_ONLY = 'cache_discovery_only'
GC_CHARSET = 'charset' GC_CHARSET = 'charset'
# Path to client_secrets.json # Path to client_secrets.json
GC_CLIENT_SECRETS_JSON = 'client_secrets_json' GC_CLIENT_SECRETS_JSON = 'client_secrets_json'
# GAM config directory containing client_secrets.json, oauth2.txt, oauth2service.json, extra_args.txt # GAM config directory containing client_secrets.json, oauth2.txt,
# oauth2service.json, extra_args.txt
GC_CONFIG_DIR = 'config_dir' GC_CONFIG_DIR = 'config_dir'
# custmerId from gam.cfg or retrieved from Google # custmerId from gam.cfg or retrieved from Google
GC_CUSTOMER_ID = 'customer_id' GC_CUSTOMER_ID = 'customer_id'
# If debug_level > 0: extra_args[u'prettyPrint'] = True, httplib2.debuglevel = gam_debug_level, appsObj.debug = True # If debug_level > 0: extra_args[u'prettyPrint'] = True,
# httplib2.debuglevel = gam_debug_level, appsObj.debug = True
GC_DEBUG_LEVEL = 'debug_level' GC_DEBUG_LEVEL = 'debug_level'
# ID Token decoded from OAuth 2.0 refresh token response. Includes hd (domain) and email of authorized user # ID Token decoded from OAuth 2.0 refresh token response. Includes hd (domain)
# and email of authorized user
GC_DECODED_ID_TOKEN = 'decoded_id_token' GC_DECODED_ID_TOKEN = 'decoded_id_token'
# Domain obtained from gam.cfg or oauth2.txt # Domain obtained from gam.cfg or oauth2.txt
GC_DOMAIN = 'domain' GC_DOMAIN = 'domain'
# Google Drive download directory # Google Drive download directory
GC_DRIVE_DIR = 'drive_dir' GC_DRIVE_DIR = 'drive_dir'
# If no_browser is False, writeCSVfile won't open a browser when todrive is set # If no_browser is False, writeCSVfile won't open a browser when todrive is set
# and doRequestOAuth prints a link and waits for the verification code when oauth2.txt is being created # and doRequestOAuth prints a link and waits for the verification code when
# oauth2.txt is being created
GC_NO_BROWSER = 'no_browser' GC_NO_BROWSER = 'no_browser'
# oauth_browser forces usage of web server OAuth flow that proved problematic. # oauth_browser forces usage of web server OAuth flow that proved problematic.
GC_OAUTH_BROWSER = 'oauth_browser' GC_OAUTH_BROWSER = 'oauth_browser'
@ -1158,7 +1169,7 @@ GC_TLS_MAX_VERSION = 'tls_max_ver'
# Path to certificate authority file for validating TLS hosts # Path to certificate authority file for validating TLS hosts
GC_CA_FILE = 'ca_file' GC_CA_FILE = 'ca_file'
tls_min = 'TLSv1_2' if hasattr(ssl.SSLContext(), 'minimum_version') else None TLS_MIN = 'TLSv1_2' if hasattr(ssl.SSLContext(), 'minimum_version') else None
GC_Defaults = { GC_Defaults = {
GC_AUTO_BATCH_MIN: 0, GC_AUTO_BATCH_MIN: 0,
GC_BATCH_SIZE: 50, GC_BATCH_SIZE: 50,
@ -1186,7 +1197,7 @@ GC_Defaults = {
GC_CSV_HEADER_FILTER: '', GC_CSV_HEADER_FILTER: '',
GC_CSV_HEADER_DROP_FILTER: '', GC_CSV_HEADER_DROP_FILTER: '',
GC_CSV_ROW_FILTER: '', GC_CSV_ROW_FILTER: '',
GC_TLS_MIN_VERSION: tls_min, GC_TLS_MIN_VERSION: TLS_MIN,
GC_TLS_MAX_VERSION: None, GC_TLS_MAX_VERSION: None,
GC_CA_FILE: None, GC_CA_FILE: None,
} }
@ -1321,20 +1332,52 @@ CLEAR_NONE_ARGUMENT = [
'none', 'none',
] ]
# #
MESSAGE_API_ACCESS_CONFIG = 'API access is configured in your Control Panel under: Security-Show more-Advanced settings-Manage API client access' MESSAGE_API_ACCESS_CONFIG = 'API access is configured in your Control Panel' \
MESSAGE_API_ACCESS_DENIED = 'API access Denied.\n\nPlease make sure the Client ID: {0} is authorized for the API Scope(s): {1}' ' under: Security-Show more-Advanced' \
MESSAGE_GAM_EXITING_FOR_UPDATE = 'GAM is now exiting so that you can overwrite this old version with the latest release' ' settings-Manage API client access'
MESSAGE_GAM_OUT_OF_MEMORY = 'GAM has run out of memory. If this is a large G Suite instance, you should use a 64-bit version of GAM on Windows or a 64-bit version of Python on other systems.' MESSAGE_API_ACCESS_DENIED = 'API access Denied.\n\nPlease make sure the Client' \
MESSAGE_HEADER_NOT_FOUND_IN_CSV_HEADERS = 'Header "{0}" not found in CSV headers of "{1}".' ' ID: {0} is authorized for the API Scope(s): {1}'
MESSAGE_HIT_CONTROL_C_TO_UPDATE = '\n\nHit CTRL+C to visit the GAM website and download the latest release or wait 15 seconds continue with this boring old version. GAM won\'t bother you with this announcement for 1 week or you can create a file named noupdatecheck.txt in the same location as gam.py or gam.exe and GAM won\'t ever check for updates.' MESSAGE_GAM_EXITING_FOR_UPDATE = 'GAM is now exiting so that you can' \
' overwrite this old version with the' \
' latest release'
MESSAGE_GAM_OUT_OF_MEMORY = 'GAM has run out of memory. If this is a large' \
' G Suite instance, you should use a 64-bit' \
' version of GAM on Windows or a 64-bit version' \
' of Python on other systems.'
MESSAGE_HEADER_NOT_FOUND_IN_CSV_HEADERS = 'Header "{0}" not found in CSV' \
' headers of "{1}".'
MESSAGE_HIT_CONTROL_C_TO_UPDATE = '\n\nHit CTRL+C to visit the GAM website' \
' and download the latest release or wait' \
' 15 seconds continue with this boring old' \
' version. GAM won\'t bother you with this ' \
' announcement for 1 week or you can create' \
' a file named noupdatecheck.txt in the same' \
' location as gam.py or gam.exe and GAM' \
' won\'t ever check for updates.'
MESSAGE_INVALID_JSON = 'The file {0} has an invalid format.' MESSAGE_INVALID_JSON = 'The file {0} has an invalid format.'
MESSAGE_NO_DISCOVERY_INFORMATION = 'No online discovery doc and {0} does not exist locally' MESSAGE_NO_DISCOVERY_INFORMATION = 'No online discovery doc and {0} does not' \
MESSAGE_NO_TRANSFER_LACK_OF_DISK_SPACE = 'Cowardly refusing to perform migration due to lack of target drive space. Source size: {0}mb Target Free: {1}mb' ' exist locally'
MESSAGE_RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET = 'Results are too large for Google Spreadsheets. Uploading as a regular CSV file.' MESSAGE_NO_TRANSFER_LACK_OF_DISK_SPACE = 'Cowardly refusing to perform' \
MESSAGE_SERVICE_NOT_APPLICABLE = 'Service not applicable for this address: {0}. Please make sure service is enabled for user and run\n\ngam user <user> check serviceaccount\n\nfor further instructions' ' migration due to lack of target' \
MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON = 'Please run\n\ngam create project\ngam user <user> check serviceaccount\n\nto create and configure a service account.' ' drive space. Source size: {0}mb' \
MESSAGE_UPDATE_GAM_TO_64BIT = "You're running a 32-bit version of GAM on a 64-bit version of Windows, upgrade to a windows-x86_64 version of GAM" ' Target Free: {1}mb'
MESSAGE_YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE_BY = 'Your system time differs from %s by %s' MESSAGE_RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET = 'Results are too large for' \
' Google Spreadsheets.' \
' Uploading as a regular' \
' CSV file.'
MESSAGE_SERVICE_NOT_APPLICABLE = 'Service not applicable for this address:' \
' {0}. Please make sure service is enabled' \
' for user and run\n\ngam user <user> check' \
' serviceaccount\n\nfor further instructions'
MESSAGE_INSTRUCTIONS_OAUTH2SERVICE_JSON = 'Please run\n\ngam create project\n' \
'gam user <user> check ' \
'serviceaccount\n\nto create and' \
' configure a service account.'
MESSAGE_UPDATE_GAM_TO_64BIT = 'You\'re running a 32-bit version of GAM on a' \
' 64-bit version of Windows, upgrade to a' \
' windows-x86_64 version of GAM'
MESSAGE_YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE_BY = 'Your system time differs' \
' from %s by %s'
USER_ADDRESS_TYPES = ['home', 'work', 'other'] USER_ADDRESS_TYPES = ['home', 'work', 'other']
USER_EMAIL_TYPES = ['home', 'work', 'other'] USER_EMAIL_TYPES = ['home', 'work', 'other']
@ -1587,167 +1630,151 @@ LANGUAGE_CODES_MAP = {
'ak': 'ak', 'ak': 'ak',
'am': 'am', 'am': 'am',
'ar': 'ar', 'ar': 'ar',
'az': 'az', #Luo, Afrikaans, Irish, Akan, Amharic, Arabica, Azerbaijani 'az': 'az',
'be': 'be', 'be': 'be',
'bem': 'bem', 'bem': 'bem',
'bg': 'bg', 'bg': 'bg',
'bn': 'bn', 'bn': 'bn',
'br': 'br', 'br': 'br',
'bs': 'bs', 'bs': 'bs',
'ca': 'ca': 'ca',
'ca', #Belarusian, Bemba, Bulgarian, Bengali, Breton, Bosnian, Catalan
'chr': 'chr', 'chr': 'chr',
'ckb': 'ckb', 'ckb': 'ckb',
'co': 'co', 'co': 'co',
'crs': 'crs', 'crs': 'crs',
'cs': 'cs', 'cs': 'cs',
'cy': 'cy', 'cy': 'cy',
'da': 'da': 'da',
'da', #Cherokee, Kurdish (Sorani), Corsican, Seychellois Creole, Czech, Welsh, Danish
'de': 'de', 'de': 'de',
'ee': 'ee', 'ee': 'ee',
'el': 'el', 'el': 'el',
'en': 'en', 'en': 'en',
'en-gb': 'en-GB', 'en-gb': 'en-GB',
'en-us': 'en-US', 'en-us': 'en-US',
'eo': 'eo': 'eo',
'eo', #German, Ewe, Greek, English, English (UK), English (US), Esperanto
'es': 'es', 'es': 'es',
'es-419': 'es-419', 'es-419': 'es-419',
'et': 'et', 'et': 'et',
'eu': 'eu', 'eu': 'eu',
'fa': 'fa', 'fa': 'fa',
'fi': 'fi', 'fi': 'fi',
'fo': 'fo': 'fo',
'fo', #Spanish, Spanish (Latin American), Estonian, Basque, Persian, Finnish, Faroese
'fr': 'fr', 'fr': 'fr',
'fr-ca': 'fr-ca', 'fr-ca': 'fr-ca',
'fy': 'fy', 'fy': 'fy',
'ga': 'ga', 'ga': 'ga',
'gaa': 'gaa', 'gaa': 'gaa',
'gd': 'gd', 'gd': 'gd',
'gl': 'gl': 'gl',
'gl', #French, French (Canada), Frisian, Irish, Ga, Scots Gaelic, Galician
'gn': 'gn', 'gn': 'gn',
'gu': 'gu', 'gu': 'gu',
'ha': 'ha', 'ha': 'ha',
'haw': 'haw', 'haw': 'haw',
'he': 'he', 'he': 'he',
'hi': 'hi', 'hi': 'hi',
'hr': 'hr', #Guarani, Gujarati, Hausa, Hawaiian, Hebrew, Hindi, Croatian 'hr': 'hr',
'ht': 'ht', 'ht': 'ht',
'hu': 'hu', 'hu': 'hu',
'hy': 'hy', 'hy': 'hy',
'ia': 'ia', 'ia': 'ia',
'id': 'id', 'id': 'id',
'ig': 'ig', 'ig': 'ig',
'in': 'in': 'in',
'in', #Haitian Creole, Hungarian, Armenian, Interlingua, Indonesian, Igbo, in
'is': 'is', 'is': 'is',
'it': 'it', 'it': 'it',
'iw': 'iw', 'iw': 'iw',
'ja': 'ja', 'ja': 'ja',
'jw': 'jw', 'jw': 'jw',
'ka': 'ka', 'ka': 'ka',
'kg': 'kg': 'kg',
'kg', #Icelandic, Italian, Hebrew, Japanese, Javanese, Georgian, Kongo
'kk': 'kk', 'kk': 'kk',
'km': 'km', 'km': 'km',
'kn': 'kn', 'kn': 'kn',
'ko': 'ko', 'ko': 'ko',
'kri': 'kri', 'kri': 'kri',
'ku': 'ku', 'ku': 'ku',
'ky': 'ky': 'ky',
'ky', #Kazakh, Khmer, Kannada, Korean, Krio (Sierra Leone), Kurdish, Kyrgyz
'la': 'la', 'la': 'la',
'lg': 'lg', 'lg': 'lg',
'ln': 'ln', 'ln': 'ln',
'lo': 'lo', 'lo': 'lo',
'loz': 'loz', 'loz': 'loz',
'lt': 'lt', 'lt': 'lt',
'lua': 'lua': 'lua',
'lua', #Latin, Luganda, Lingala, Laothian, Lozi, Lithuanian, Tshiluba
'lv': 'lv', 'lv': 'lv',
'mfe': 'mfe', 'mfe': 'mfe',
'mg': 'mg', 'mg': 'mg',
'mi': 'mi', 'mi': 'mi',
'mk': 'mk', 'mk': 'mk',
'ml': 'ml', 'ml': 'ml',
'mn': 'mn': 'mn',
'mn', #Latvian, Mauritian Creole, Malagasy, Maori, Macedonian, Malayalam, Mongolian
'mo': 'mo', 'mo': 'mo',
'mr': 'mr', 'mr': 'mr',
'ms': 'ms', 'ms': 'ms',
'mt': 'mt', 'mt': 'mt',
'my': 'my', 'my': 'my',
'ne': 'ne', 'ne': 'ne',
'nl': 'nl', #Moldavian, Marathi, Malay, Maltese, Burmese, Nepali, Dutch 'nl': 'nl',
'nn': 'nn', 'nn': 'nn',
'no': 'no', 'no': 'no',
'nso': 'nso', 'nso': 'nso',
'ny': 'ny', 'ny': 'ny',
'nyn': 'nyn', 'nyn': 'nyn',
'oc': 'oc', 'oc': 'oc',
'om': 'om': 'om',
'om', #Norwegian (Nynorsk), Norwegian, Northern Sotho, Chichewa, Runyakitara, Occitan, Oromo
'or': 'or', 'or': 'or',
'pa': 'pa', 'pa': 'pa',
'pcm': 'pcm', 'pcm': 'pcm',
'pl': 'pl', 'pl': 'pl',
'ps': 'ps', 'ps': 'ps',
'pt-br': 'pt-BR', 'pt-br': 'pt-BR',
'pt-pt': 'pt-pt': 'pt-PT',
'pt-PT', #Oriya, Punjabi, Nigerian Pidgin, Polish, Pashto, Portuguese (Brazil), Portuguese (Portugal)
'qu': 'qu', 'qu': 'qu',
'rm': 'rm', 'rm': 'rm',
'rn': 'rn', 'rn': 'rn',
'ro': 'ro', 'ro': 'ro',
'ru': 'ru', 'ru': 'ru',
'rw': 'rw', 'rw': 'rw',
'sd': 'sd': 'sd',
'sd', #Quechua, Romansh, Kirundi, Romanian, Russian, Kinyarwanda, Sindhi
'sh': 'sh', 'sh': 'sh',
'si': 'si', 'si': 'si',
'sk': 'sk', 'sk': 'sk',
'sl': 'sl', 'sl': 'sl',
'sn': 'sn', 'sn': 'sn',
'so': 'so', 'so': 'so',
'sq': 'sq': 'sq',
'sq', #Serbo-Croatian, Sinhalese, Slovak, Slovenian, Shona, Somali, Albanian
'sr': 'sr', 'sr': 'sr',
'sr-me': 'sr-ME', 'sr-me': 'sr-ME',
'st': 'st', 'st': 'st',
'su': 'su', 'su': 'su',
'sv': 'sv', 'sv': 'sv',
'sw': 'sw', 'sw': 'sw',
'ta': 'ta': 'ta',
'ta', #Serbian, Montenegrin, Sesotho, Sundanese, Swedish, Swahili, Tamil
'te': 'te', 'te': 'te',
'tg': 'tg', 'tg': 'tg',
'th': 'th', 'th': 'th',
'ti': 'ti', 'ti': 'ti',
'tk': 'tk', 'tk': 'tk',
'tl': 'tl', 'tl': 'tl',
'tn': 'tn', #Telugu, Tajik, Thai, Tigrinya, Turkmen, Tagalog, Setswana 'tn': 'tn',
'to': 'to', 'to': 'to',
'tr': 'tr', 'tr': 'tr',
'tt': 'tt', 'tt': 'tt',
'tum': 'tum', 'tum': 'tum',
'tw': 'tw', 'tw': 'tw',
'ug': 'ug', 'ug': 'ug',
'uk': 'uk', #Tonga, Turkish, Tatar, Tumbuka, Twi, Uighur, Ukrainian 'uk': 'uk',
'ur': 'ur', 'ur': 'ur',
'uz': 'uz', 'uz': 'uz',
'vi': 'vi', 'vi': 'vi',
'wo': 'wo', 'wo': 'wo',
'xh': 'xh', 'xh': 'xh',
'yi': 'yi', 'yi': 'yi',
'yo': 'yo', #Urdu, Uzbek, Vietnamese, Wolof, Xhosa, Yiddish, Yoruba 'yo': 'yo',
'zh-cn': 'zh-CN', 'zh-cn': 'zh-CN',
'zh-hk': 'zh-HK', 'zh-hk': 'zh-HK',
'zh-tw': 'zh-TW', 'zh-tw': 'zh-TW',
'zu': 'zu': 'zu',
'zu', #Chinese (Simplified), Chinese (Hong Kong/Traditional), Chinese (Taiwan/Traditional), Zulu
} }
# maxResults exception values for API list calls. Should only be listed if: # maxResults exception values for API list calls. Should only be listed if: